المجموعات — تجميع العناصر دون تكرار
إزالة التكرارات، الرموز الأربعة لمقارنة مجموعتين، لماذا لا يمثل {} مجموعة فارغة، وما يعنيه عدم وجود ترتيب في المجموعات عملياً.
- 1المشكلة
- 2الفهم
- 3أمثلة محلولة
- 4التوقع
- 5التطبيق
- 6التحدي
المشكلة التي نقوم بحلها
تصلنا مبيعات أحد الأيام في صورة قائمة، تحتوي على عنصر لكل عملية بيع:
sold = ["pen", "bag", "pen", "ink", "pen"]السؤال المطروح: كم منتجاً مختلفاً تم بيعه؟ تخبرنا len(sold) بأن العدد خمسة، لكن هذا يمثل عدد عمليات البيع الإجمالية. في الواقع، هناك ثلاثة منتجات متميزة وفريدة فقط.
الأدوات التي درسناها في الدروس السابقة يمكنها حل هذه المسألة — نبدأ بقائمة فارغة، ولكل عنصر نفحص if item not in unique: قبل إضافته. ستة أسطر برمجية وتفي بالغرض تماماً.
لكن ماذا لو كان السؤال: "ما هي المنتجات التي بيعت في كل من يومي الاثنين والثلاثاء معاً؟" ستحتاج حينها إلى حلقتين تكراريتين متداخلتين. وماذا عن "المنتجات التي بيعت يوم الاثنين فقط؟" — ستحتاج حلقة أخرى! تتكرر هذه الأسئلة بكثرة لدرجة أن لها فرعاً كاملاً مخصصاً في علم الرياضيات (نظرية المجموعات)، وفي بايثون يمكن كتابة هذه العمليات الرياضية مباشرة وبأبسط شكل ممكن.
المجموعة (Set) هي الوعاء المثالي لذلك: فهي لا تسمح بأي تكرار، وتجيب عن أسئلة مثل "العناصر المشتركة في كليهما" أو "الموجودة في أحدهما دون الآخر" باستخدام رمز رياضي واحد فقط.
في نهاية هذا الدرس ستكون قادراً على
- إنشاء مجموعة (set) واستخراج العناصر الفريدة من القائمة بحذف التكرارات
- مقارنة مجموعتين باستخدام المعاملات
|و&و-و^ - شرح سبب عدم اعتبار
{}مجموعة فارغة - إدراك حقيقة أن المجموعات لا تحتوي على ترتيب داخلي، ومعرفة متى يكون ذلك مهماً
- كتابة النمط البرمجي الذي يزيل التكرار مع الحفاظ على الترتيب الأصلي للعناصر
المتطلبات السابقة: القواميس — أزواج المفاتيح والقيم.
المجموعة — تجميع خالٍ من التكرار
sold = ["pen", "bag", "pen", "ink", "pen"]
unique = set(sold)
print(sorted(unique))
print(len(unique))['bag', 'ink', 'pen']
3تستقبل الدالة set(...) قائمة وتقوم بإسقاط كافة العناصر المكررة منها. فتحولت الكلمات الثلاث pen إلى عنصر واحد فقط.
لاحظ استخدام sorted() أثناء الطباعة، وهو أمر مقصود تماماً — وسيتضح سببه بعد قليل.
يمكنك أيضاً إنشاء مجموعة مباشرة باستخدام الأقواس المعقوفة:
letters = {"a", "b", "a"}
print(sorted(letters))
print(len(letters))['a', 'b']
2اختفت القيمة الثانية "a" بهدوء ودون أي رسالة خطأ — فالمجموعة بطبيعتها لا تفهم مفهوم وجود نفس العنصر مرتين.
الرمز {} ليس مجموعة فارغة!
not_a_set = {}
real_set = set()
print(type(not_a_set))
print(type(real_set))<class 'dict'>
<class 'set'>ارتبطت الأقواس المعقوفة في بايثون بالقواميس أولاً، لذلك بقي الرمز الفارغ {} محجوزاً للقواميس الفارغة. ولإنشاء مجموعة فارغة، يجب عليك كتابة set().
والخطأ هنا صامت ومخادع: تبدأ بكتابة {}، ثم تستدعي .add()، فتحصل على خطأ AttributeError يجعلك تظن أن المشكلة في دالة add نفسها!
لا يوجد ترتيب داخلي
هذا هو القيد الأكثر أهمية في المجموعات، والخاصية الأكثر تعرضاً لسوء الفهم.
العناصر داخل المجموعة لا تخضع لأي ترتيب. ولا توجد فهارس رقمية ولا شرائح اقتطاع:
tags = {"new", "sale"}
print(tags[0])TypeError: 'set' object is not subscriptableكما أن الترتيب الذي تراه عند طباعة المجموعة ليس ثابتاً ولا يمكن الاعتماد عليه — فقد تشغّل نفس البرنامج مرتين وتظهر العناصر بترتيب مختلف! ولهذا السبب تقوم جميع الأمثلة في هذا الدرس بترتيب المجموعة باستخدام sorted() قبل الطباعة: فما تراه عند طباعة المجموعة مباشرة ليس مضموناً أن يتكرر بنفس الشكل في المرة القادمة.
أقصر طريقة لإزالة التكرارات من قائمة هي list(set(sold))، لكنها تطيح بالترتيب الأصلي للعناصر. وعندما يكون الترتيب مهماً، نستخدم قائمة ومجموعة معاً:
sold = ["pen", "bag", "pen", "ink", "bag"]
seen = set()
unique = []
for item in sold:
if item not in seen:
seen.add(item)
unique.append(item)
print(unique)['pen', 'bag', 'ink']تحتفظ القائمة بترتيب الظهور، بينما تتذكر المجموعة ما تم المرور عليه مسبقاً. هذا الدمج بينهما نمط برمجي شائع وعملي للغاية.
الإضافة والحذف
tags = {"new"}
tags.add("sale")
tags.add("new")
print(sorted(tags))
tags.discard("gone")
tags.remove("sale")
print(sorted(tags))['new', 'sale']
['new']تضيف الدالة add عنصراً واحداً — وإذا كان العنصر موجوداً بالفعل، فلن يحدث شيء ولن يعترض بايثون. دالة append في القوائم كانت ستضيف نسخة ثانية؛ أما المجموعة فلا تفعل ذلك.
أما الحذف فيأتي في صورتين، والفرق بينهما مفيد جداً:
tags = {"new"}
tags.remove("gone")KeyError: 'gone'تتوقف دالة remove وتطلق خطأ عندما لا تجد العنصر المطلوب؛ بينما تتجاهل دالة discard الأمر بهدوء إذا لم تجده. فإذا كان العنصر يُفترض به أن يكون موجوداً حتماً، فاكتب remove لكي يُكتشف أي خطأ على الفور.
مقارنة مجموعتين
هنا تبرز القوة الحقيقية للمجموعات:
monday = {"rafi", "ahmed", "bilal"}
tuesday = {"ahmed", "dia"}
print(sorted(monday | tuesday))
print(sorted(monday & tuesday))
print(sorted(monday - tuesday))
print(sorted(monday ^ tuesday))['ahmed', 'bilal', 'dia', 'rafi']
['ahmed']
['bilal', 'rafi']
['bilal', 'dia', 'rafi']أربعة رموز لأربعة أسئلة جوهرية:
|— الاتحاد (Union): العناصر الموجودة في أحدهما أو كلاهما. أي كل من حضر عبر اليومين.&— التقاطع (Intersection): العناصر المشتركة في كلاهما معاً. أي من حضر في كلا اليومين.-— الفرق (Difference): العناصر الموجودة في الأولى وليست في الثانية. أي من حضر يوم الاثنين فقط.^— الفرق المتناظر (Symmetric Difference): العناصر الموجودة في إحداهما فقط دون الأخرى. أي من حضر في يوم واحد فقط وليس في اليومين معاً.
لاحظ أن علامة الطرح - تهتم بالترتيب: فالعملية monday - tuesday تعطي نتيجة مختلفة عن tuesday - monday. أما العمليات الثلاث الأخرى فتبادلية وتعطي نفس النتيجة في الاتجاهين.
كتابة هذه العمليات الأربع باستخدام الحلقات التكرارية العادية كانت ستتطلب أسطراً برمجية كثيرة لكل منها، مع فرصة كبيرة للوقوع في خطأ برمجي.
ما الذي يمكن وضعه داخل المجموعة؟
تنطبق هنا نفس القاعدة الخاصة بمفاتيح القواميس: يجب أن يكون أي شيء تضعه داخل المجموعة كائناً غير قابل للتعديل (immutable).
s = set()
s.add([1, 2])TypeError: unhashable type: 'list'نفس رسالة الخطأ بالحرف الواحد وللسبب ذاته — فالمجموعات تعتمد داخلياً على تقنية التجزئة (hashing). النصوص والأرقام والصفوف مقبولة تماماً؛ أما القوائم والقواميس فلا يمكن وضعها داخل المجموعات.
مثال متكامل
sales.py:
# Which products sold on both days, and which only on one
monday = ["pen", "bag", "pen", "ink"]
tuesday = ["ink", "bottle", "pen", "ink"]
mon = set(monday)
tue = set(tuesday)
print("Monday sold :", sorted(mon), f"({len(monday)} sales, {len(mon)} products)")
print("Tuesday sold :", sorted(tue), f"({len(tuesday)} sales, {len(tue)} products)")
print()
print("Either day :", sorted(mon | tue))
print("Both days :", sorted(mon & tue))
print("Monday only :", sorted(mon - tue))
print("Exactly one day:", sorted(mon ^ tue))
print()
print("Was ink sold on Monday?", "ink" in mon)Monday sold : ['bag', 'ink', 'pen'] (4 sales, 3 products)
Tuesday sold : ['bottle', 'ink', 'pen'] (4 sales, 3 products)
Either day : ['bag', 'bottle', 'ink', 'pen']
Both days : ['ink', 'pen']
Monday only : ['bag']
Exactly one day: ['bag', 'bottle']
Was ink sold on Monday? Trueثلاث ملاحظات مهمة:
تم الاحتفاظ بالقوائم الأصلية. تخبرنا len(monday) بوجود أربع عمليات بيع، بينما تخبرنا len(mon) بوجود ثلاثة منتجات فقط. بمجرد التحويل إلى مجموعة نفقد عدد المبيعات الإجمالي — لذا فإن المجموعة ليست بديلاً عن البيانات الأصلية، بل هي أداة متخصصة للإجابة عن نوع معين من الأسئلة حولها.
استخدام sorted() قبل كل عملية طباعة. من دونه سيبدو الترتيب عشوائياً، وقد يختلف بين كل تشغيل وآخر.
كتابة "ink" in mon بدلاً من "ink" in monday. كلاهما يعيد True، لكن البحث في المجموعة أسرع بكثير من البحث في القائمة — ففي القائمة تقارن بايثون العناصر واحداً تلو الآخر بالترتيب، بينما في المجموعة تذهب مباشرة إلى موقع العنصر في الذاكرة عبر دالة التجزئة (hashing). مع أربعة عناصر لا يشكل ذلك فارقاً ملحوظاً، لكن مع أربعين ألف عنصر يصبح الفارق هائلاً.
حالات الخطأ الشائعة
AttributeError: 'dict' object has no attribute 'add' كتابة s = {} بقصد إنشاء مجموعة، لكن هذا ينشئ قاموساً فارغاً. اكتب s = set().
TypeError: 'set' object is not subscriptable محاولة كتابة s[0]. المجموعات لا تحتوي على ترتيب داخلي، وبالتالي لا تملك فهارس رقمية. إذا كنت بحاجة إلى عنصر معين، فربما تحتاج إلى قائمة أو قاموس؛ وإلا فإن sorted(s) يعطيك قائمة مرتبة منها.
KeyError: 'gone' طلبت دالة remove حذف عنصر غير موجود بالمجموعة. إذا كان عدم وجوده أمراً محتملاً وطبيعياً، فاستخدم discard.
TypeError: unhashable type: 'list' محاولة وضع قائمة داخل مجموعة. حوّل القائمة أولاً إلى صف باستخدام tuple(...).
تبعثر ترتيب العناصر بعد استخدام list(set(...)) هذا متوقع تماماً؛ فالمجموعات لا تحفظ الترتيب. إذا كنت بحاجة للحفاظ على الترتيب الأصلي، فاستخدم نمط التتبع seen الموضح أعلاه، أو استخدم sorted().
مقارنة مجموعتين بـ == أعطت True رغم اختلاف الترتيب الظاهري هذا سلوك صحيح تماماً. تتساوى مجموعتان إذا كانتا تحتويان على نفس العناصر تماماً — ولا يلعب الترتيب أي دور في ذلك. فالمقارنة [1, 2] == [2, 1] تعطي False في القوائم، بينما {1, 2} == {2, 1} تعطي True في المجموعات.
Step 4 of 6 — Predict
Check your understanding
Two empty containers are made. What does type() report?
not_a_set = {}
real_set = set()
print(type(not_a_set))
print(type(real_set))- A<class 'dict'> <class 'set'>
- B<class 'set'> <class 'set'>
- C<class 'set'> <class 'dict'>
- D<class 'dict'> <class 'dict'>
Which two questions do & and - answer?
monday = {"rafi", "ahmed", "bilal"}
tuesday = {"ahmed", "dia"}
print(sorted(monday & tuesday))
print(sorted(monday - tuesday))- A['ahmed'] ['bilal', 'rafi']
- B['bilal', 'rafi'] ['ahmed']
- C['ahmed'] ['dia']
- D['ahmed', 'bilal', 'dia', 'rafi'] ['ahmed']
The first item of a set is asked for. What happens?
tags = {"new", "sale"}
print(tags[0])- AA `TypeError` — a set has no order, so it has no indexes
- Bnew
- Csale
- DA `KeyError`
Answering needs an account
Sign in to check your answers
The questions are above, and working them out in your head is the part that matters. Sign in to see the answers, the explanations and the three-level hints.
دورك الآن
اكتب ملفاً باسم visitors.py يحتوي على ثلاث قوائم للزوار، قائمة لكل يوم، مع تكرار بعض الأسماء عبر الأيام المختلفة.
ثم اطبع:
- عدد الزيارات لكل يوم، وعدد الزوار المتميزين (الفريدين) في كل يوم
- عدد الزوار المتميزين عبر الأيام الثلاثة مجتمعة
- قائمة بأسماء الأشخاص الذين حضروا في جميع الأيام الثلاثة
- قائمة بالأشخاص الذين حضروا في اليوم الأول فقط
- الأشخاص الذين حضروا في يوم واحد فقط من أول يومين (دون اليوم الآخر)
ثم أجرِ هاتين التجربتين:
- أزل التكرارات بطريقتين — باستخدام
list(set(...))وباستخدام نمطseenالموضح أعلاه — واطبع النتيجتين جنباً إلى جنب. ما الفرق بينهما؟ - حاول إنشاء مجموعة فارغة باستخدام
{}واستدعِ.add()عليها. ما هو الخطأ الذي سيظهر، وما هو نوع الكائن المذكور في رسالة الخطأ؟
التجربة الثانية بسيطة وسريعة، ورؤيتها بعينيك مرة واحدة تعفيك من إضاعة دقائق ثمينة في البحث عن سبب الخطأ مستقبلاً.
Step 6 of 6
التحدي — the chapter quiz
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
Sign in to take the quiz