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. מה יכול להיכנס לקבוצה#

איברי קבוצה חייבים להיות ניתנים לגיבוב (hashable) – אותה מגבלה כמו עבור מפתחות 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) בלתי-משתנה של קבוצה קיימת:

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

שתי סיבות נפוצות לפנות אליה:

  • שימוש כמפתח מילון או כאיבר קבוצה. בכל מקום שערך יחיד אינו יכול לתפוס את מה שאתם צריכים, frozenset של ערכים יכולה – ”קבוצת המאפיינים הנתמכים על ידי דרייבר זה“, ”קבוצת הפינים שפרופיל זה משתמש בהם“.

  • נעילת קבוע. frozenset ברמת המודול של שמות מורשים אינה יכולה להשתנות בטעות על ידי קורא; set רגילה כן. העדיפו frozenset עבור כל דבר שאמור להיות לקריאה בלבד לאחר הבנייה.