2.10. المجموعات

المجموعة هي تجميعة غير مرتّبة من عناصر فريدة. وإضافة قيمة موجودة بالفعل ليس لها أثر؛ والتكرار يُنتج كل قيمة مرة واحدة بالضبط. والمجموعات هي الأداة المناسبة عندما تهمّ العضوية وإزالة التكرار، ولا يهمّ الترتيب.

2.10.1. إنشاء مجموعة

استخدم الأقواس المعقوفة لمجموعة غير فارغة، أو set() لمجموعة فارغة:

colours = {"red", "green", "blue"}
empty = set()

تبدو الأقواس المعقوفة كأنها قاموس dict حرفي؛ و {} وحدها هي قاموس فارغ، وليست مجموعة فارغة -- وهذه إحدى صدف Python التاريخية. استخدم set() للحالة الفارغة.

set() يبني أيضًا مجموعة من أي كائن قابل للتكرار، وهي الطريقة القياسية لإسقاط التكرارات من تسلسل:

nums = [1, 2, 2, 3, 1, 4]
unique = set(nums)
print(unique)

الخرج:

{1, 2, 3, 4}

قد يتفاوت ترتيب الطباعة -- فالمجموعات لا تَعِد بالتكرار بأي ترتيب معيّن.

2.10.2. المجموعة مقابل القاموس

تخزّن كل من المجموعات والقواميس عناصر فريدة في جدول تجزئة. والفرق هو فيما يحمله كل عنصر معه:

  • dict يخزّن أزواج مفتاح-قيمة. والبحث عن مفتاح يُعيد قيمته.

  • set يخزّن العناصر فقط. والبحث عن عنصر يخبرك بما إذا كان موجودًا.

الاختيار بين الاثنين يتعلق بما إذا كانت القيمة المرافقة لكل عنصر تعني شيئًا:

  • اختر مجموعة عندما لا تكون هناك قيمة تنتمي بجانب كل عنصر -- إذ يهمّك فقط ما إذا كان العنصر موجودًا، أو أنك تجمع مجموعات من العناصر الفريدة باستخدام الاتحاد / التقاطع.

  • اختر قاموسًا عندما يكون كل عنصر مقترنًا ببيانات يُقصد من البحث استرجاعها -- خريطة إعدادات، أو ذاكرة تخزين مؤقت، أو عدّاد مفهرس بالاسم.

يتشارك النوعان قدرًا كبيرًا من الصيغة الظاهرية، وهنا منشأ معظم الالتباس. الفروق في كتلة واحدة:

set

dict

يحمل

عناصر فريدة

مفاتيح فريدة، لكل منها قيمة

حرفي ممتلئ

{1, 2, 3}

{"a": 1, "b": 2}

حرفي فارغ

set()

{}

اختبار العضوية

x in s

k in d (المفاتيح فقط)

جلب قيمة

غير متاح

d[k]

إضافة عنصر

s.add(x)

d[k] = v

التكرار

يُنتج عناصر

يُنتج مفاتيح (استخدم d.items() للأزواج)

عدم التماثل بين الحرفي الممتلئ والفارغ هو المطبّ الجدير بالتنبيه إليه:

  • الأقواس التي بداخلها عناصر -- {1, 2, 3} -- هي مجموعة حرفية؛ والأقواس التي بداخلها أزواج مفتاح-قيمة -- {"a": 1} -- هي قاموس حرفي. ويميّز المحلّل بينها بحسب ما بداخلها.

  • الأقواس التي لا شيء بداخلها -- {} -- هي قاموس فارغ، وليست مجموعة فارغة. فالقواميس جاءت أولًا؛ والحرفي الفارغ يخصّها. أما المجموعة الفارغة فليس لها حرفي بأقواس على الإطلاق ويجب كتابتها set().

من الأنماط الشائعة عندما لا تُقرأ سوى مفاتيح القاموس أبدًا أن يُتحوَّل إلى مجموعة -- فهذا يجعل النية واضحة ويقتطع القيم غير المستخدمة من الذاكرة.

2.10.3. الإضافة والإزالة

  • set.add() -- يُدرج عنصرًا واحدًا.

  • set.discard() -- يزيل عنصرًا إذا كان موجودًا، ولا يفعل شيئًا إذا لم يكن كذلك.

  • set.remove() -- يزيل عنصرًا؛ ويثير KeyError إذا كان مفقودًا.

  • set.clear() -- يُفرغ المجموعة.

s = {1, 2, 3}
s.add(4)
s.discard(99)            # silent: 99 not in s
s.remove(2)
print(s)

الخرج:

{1, 3, 4}

2.10.4. العضوية

يختبر العامل in العضوية. وعلى المجموعة يكون زمنه ثابتًا تقريبًا بصرف النظر عن الحجم -- وهذا هو السبب الرئيسي لاختيار مجموعة بدلًا من list عندما تحتاج فقط إلى السؤال "هل هذه القيمة موجودة هناك":

if "red" in colours:
    print("colour is allowed")

list بالمحتويات نفسها سيمسح من البداية في كل مرة، وهو أمر لا بأس به لعشرة عناصر لكنه بطيء لعشرة آلاف.

2.10.5. عمليات المجموعات

يمكن دمج مجموعتين بالعمليات الرياضية المعتادة. ولكل منها صورة بعامل وصورة بدالة:

  • a | b أو a.union(b) -- كل ما في أي من المجموعتين.

  • a & b أو a.intersection(b) -- فقط ما يظهر في كلتيهما.

  • a - b أو a.difference(b) -- ما في a لكن ليس في b.

  • a ^ b أو a.symmetric_difference(b) -- ما في إحداهما لكن ليس في كلتيهما.

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a | b)
print(a & b)
print(a - b)
print(a ^ b)

الخرج:

{1, 2, 3, 4, 5, 6}
{3, 4}
{1, 2}
{1, 2, 5, 6}

صور العوامل للقراءة فقط؛ أما صور الدوال فتقبل أي كائن قابل للتكرار على اليمين، وليس مجرد مجموعة أخرى (a.union([5, 6])). اختر ما يقرأ بشكل أفضل في السياق.

2.10.6. ما الذي يصلح في المجموعة

يجب أن تكون عناصر المجموعة قابلة للتجزئة -- وهو القيد نفسه المفروض على مفاتيح dict. وتعمل كلها int و float و str و bool و bytes و tuple (عندما تكون محتوياته قابلة للتجزئة بذاتها). أما list و dict فلا؛ ومحاولة إضافة أحدهما تثير TypeError.

2.10.7. frozenset

set العادية قابلة للتغيير: فكل استدعاء لـ add / remove / discard يغيّر الكائن في مكانه. وهذه القابلية للتغيير تُسقط أهليتها لأن تكون قابلة للتجزئة، لذا لا يمكن استخدام مجموعة كمفتاح dict أو كعنصر في مجموعة أخرى.

frozenset هي النظير غير القابل للتغيير. فلها عمليات البحث والعوامل نفسها (in و | و & و - و ^) كما في set، لكن بلا add / remove وبلا أي دوال تغيّر. ولأنه لا شيء يمكن أن يغيّر محتوياتها أبدًا، فإن تجزئة frozenset محددة جيدًا -- فهي إذًا قابلة للتجزئة:

primary = frozenset({"red", "green", "blue"})
secondary = frozenset({"yellow", "purple", "orange"})

palettes = {
    primary: "RGB",
    secondary: "mixed",
}

print(palettes[primary])

الخرج:

RGB

أنشئ frozenset من أي كائن قابل للتكرار -- frozenset() للحالة الفارغة، و frozenset(some_set) لأخذ لقطة غير قابلة للتغيير لمجموعة موجودة:

snapshot = frozenset(s)         # immutable copy of s
s.add("new")                    # snapshot does not change

سببان شائعان للجوء إليها:

  • استخدامها كمفتاح قاموس أو عنصر مجموعة. في أي مكان تعجز فيه قيمة واحدة عن استيعاب ما تحتاجه، يمكن لـ frozenset من القيم أن تفعل ذلك -- "مجموعة الميزات التي يدعمها هذا المُشغّل"، "مجموعة الدبابيس التي يستخدمها هذا الملف الشخصي".

  • تثبيت ثابت. frozenset على مستوى الوحدة من الأسماء المسموح بها لا يمكن أن يغيّرها مستدعٍ عن طريق الخطأ؛ بينما يمكن ذلك مع set العادية. ففضّل frozenset لأي شيء يُقصد به أن يكون للقراءة فقط بعد الإنشاء.