קבוצות

הגדרה

קבוצה (או set) בפייתון היא אוסף של איברים שמתאפיינת בתכונות הבאות:

  • לאיברים באוסף אין סדר מוגדר.
  • ערך לא יכול להופיע יותר מפעם אחת.
  • כל איבר חייב להיות immutable.

לדוגמה:

  • כתובות הדואר האלקטרוני של כל הזכאים לתעודת סיום בקורס פייתון.
  • המספרים הראשוניים עד 1,000.
  • שמות כל הקבצים בתיקייה images.
  • המילים בספר "קפקא על החוף", ללא כפילויות.

שימוש

הבסיס

עד מחברת זו, לו היינו מתבקשים להגדיר מבנה נתונים שיאחסן עבורנו שמות של אומנים איטלקים ידועים, היינו משתמשים ברשימה או ב־tuple־ים:


In [ ]:
# List:
italian_artists = ['Donatelo', 'Giotto', 'Leonardo', 'Masaccio', 'Michelangelo', 'Raphael', 'Titian']
# Tuple:
italian_artists = ('Donatelo', 'Giotto', 'Leonardo', 'Masaccio', 'Michelangelo', 'Raphael', 'Titian')

אך כיוון שמדובר באוסף איברים ללא חזרות וללא סדר מוגדר, אפשר לשמור אותם כ־set:


In [ ]:
italian_artists = {'Donatelo', 'Giotto', 'Leonardo', 'Masaccio', 'Michelangelo', 'Raphael', 'Titian'}

אפשר לראות שההגדרות דומות מאוד במראה שלהן.
כדי להגדיר קבוצה, נפתח סוגריים מסולסלים (כמו במילון), ונציין את איבריה, כשהם מופרדים זה מזה בפסיקים.
אם תרצו, תוכלו לדמיין קבוצה כמילון שבו יש רק מפתחות.

נוכל גם להגדיר קבוצה ריקה באמצעות הפונקציה set.
נגדיר, לדוגמה, את קבוצת כל החדי־קרן הוורודות הבלתי נראות:


In [ ]:
invisible_pink_unicorns = set()
print(invisible_pink_unicorns)

תעלול נחמד נוסף הוא המרת iterable לקבוצה, באמצעות אותה הפונקציה:


In [ ]:
numbers = range(10)
numbers = set(numbers)
print(numbers)

בהגדרה שבתחילת הפרק ציינו שקבוצה לא תכיל כמה איברים בעלי ערך זהה.
למרות ההגבלה הזו, בהגדרת הקבוצה אפשר לספק ערכים שחוזרים על עצמם.
הקבוצה "תבלע" את הכפילויות בערכים, ותשאיר רק איבר אחד מכל ערך:


In [ ]:
print({1, 1, 2, 3, 5})
print(set('abccccccccccc'))

הגדירו קבוצה שאיבריה הם האותיות הקטנות והגדולות באנגלית.
ישנו פתרון פשוט שלא מצריך מכם לכתוב את כל הא"ב.

חשוב!
פתרו לפני שתמשיכו!

פעולות בין קבוצות

נגדיר שוב את קבוצת האומנים האיטלקים:


In [ ]:
italian_artists = {'Donatelo', 'Giotto', 'Leonardo', 'Masaccio', 'Michelangelo', 'Raphael', 'Titian'}

אחד מיתרונותיהן רבי־הערך של קבוצות הוא היכולת לבצע ביניהן פעולות בקלות.
נדגים את הרעיון בעזרת הגדרת קבוצה נוספת, הפעם של דמויות מחוברות הקומיקס של צבי הנינג'ה:


In [ ]:
ninja_turtles_characters = {'Donatelo', 'Leonardo', 'Michelangelo', 'Raphael', 'Splinter', 'Shredder'}

כעת נוכל, לדוגמה, לבחור את כל הדמויות מצבי הנינג'ה שנושאות שם של אומן איטלקי מפורסם.
הקבוצה שנוצרת מהאיברים המשותפים לשתי הקבוצות נקראת קבוצת החיתוך.


In [ ]:
ninja_turtles_characters.intersection(italian_artists)

ישנן פעולות רבות נוספות שאפשר לבצע בין קבוצות.
דרך קלה להמחיש את היחסים ההדדיים בין הקבוצות הללו היא תרשים שנקרא "דיאגרמת ון":

דיאגרמת ון של שתי קבוצות: דמויות מצבי הנינג'ה ואומנים איטלקים.
אפשר לראות בשטח החפיפה שבין שני העיגולים את האיברים המשותפים לשתי הקבוצות.

נוכל גם לבקש, לדוגמה, את שמות האומנים האיטלקים שאינם דמויות מצבי הנינג'ה.
נקבל בחזרה את השמות שמופיעים בתרשים בעיגול הטורקיז, אך לא בחיתוך בין שני העיגולים:


In [ ]:
italian_artists.difference(ninja_turtles_characters)

באותה צורה נוכל לבקש את שמות כל הדמויות מצבי הנינג'ה שאינם אומנים איטלקים.
אלו השמות שמופיעים אך ורק בעיגול הורוד, ולא בחיתוך בין שני העיגולים:


In [ ]:
ninja_turtles_characters.difference(italian_artists)

אפשר לבקש גם את קבוצת האיחוד, שמורכבת מכל הדמויות שקיימות בשתי הקבוצות.
אלו כל הדמויות שמופיעות בשרטוט:


In [ ]:
ninja_turtles_characters.union(italian_artists)

או את כל הדמויות שלא משותפות לשתי הקבוצות.
אלו כל הדמויות שמופיעות מחוץ לחיתוך של שני העיגולים:


In [ ]:
ninja_turtles_characters.symmetric_difference(italian_artists)

צרו קבוצה שאיבריה הם 10 מספרי פיבונאצ'י הראשונים, וקבוצה נוספת שאיבריה הם המספרים הזוגיים בין 1 ל־15.
שרטטו דיאגרמת ון של שתי הקבוצות, ובדקו שהשרטוט נאמן למציאות בעזרת הפעולות שלמדתם למעלה.

תרגיל ביניים: נענועי חנית כנגד קוף

בתיקיית resources נמצאים הקבצים hamlet.txt ו־the-monkeys-paw.txt.
כמה מילים משותפות לשני החיבורים? כמה שונות?
כמה מהמילים מופיעות רק בכפת הקוף, וכמה רק בהמלט?

עריכת קבוצה

קבוצה היא mutable – אפשר לערוך אותה מבלי ליצור קבוצה חדשה, בדומה לרשימות או למילונים.
תכונה זו מאפשרת לנו לצרף לקבוצה איברים חדשים או להסיר ממנה איברים קיימים בקלות רבה.

נוסיף לקבוצת שמות הדמויות בצבי הנינג'ה את אפריל או'ניל באמצעות הפעולה add:


In [ ]:
ninja_turtles_characters.add("April O'Neil")
print(ninja_turtles_characters)

ונמחק מהקבוצה את שרדר הרשע באמצעות הפעולה remove:


In [ ]:
ninja_turtles_characters.remove("Shredder")
print(ninja_turtles_characters)

שימו לב שניסיון למחוק ערך שלא קיים בקבוצה יכעיס את פייתון:


In [ ]:
ninja_turtles_characters.remove("Krang")

אלא אם כן נשתמש בפעולה discard, שמאפשרת לנו למחוק איברים מקבוצות, בלי לחשוש משגיאה:


In [ ]:
ninja_turtles_characters.discard("Krang")
print(ninja_turtles_characters)

ולסיום – הדמויות הרעות תמיד חוזרות לסיבוב שני.
נרחיב את הקבוצה שלנו ונוסיף אליה את הנבלים מ"שבט הרגל" (בחיי, ככה הם תרגמו את Foot Clan!) במכה אחת, באמצעות הפעולה update:


In [ ]:
foot_clan_members = {'Shredder', 'Krang', 'Karai'}
ninja_turtles_characters.update(foot_clan_members)
print(ninja_turtles_characters)

צרו קבוצה של 100 המספרים הראשוניים הראשונים, וצרפו אליה את כל המספרים הזוגיים בין 1 ל־1,000.
צרו קבוצה נוספת של המספרים בין 1 ל־1,000 שסכום ספרותיהם קטן מ־4.
כמה מספרים משותפים לשתי הקבוצות שיצרתם?

בדיקות שייכות

אחת התכונות המועילות ביותר של קבוצות היא שאפשר לבצע בעזרתן בדיקות שייכות במהירות רבה.
בדיוק כמו חיפוש מפתח במילון, בקבוצה אפשר למצוא ערך בתוך זמן קצר מאוד.


In [ ]:
only_turtles = {'Donatelo', 'Raphael', 'Michelangelo', 'Leonardo'}

In [ ]:
'Donatelo' in only_turtles

אפשר גם לבדוק אם כל האיברים של קבוצה אחת נמצאים בתוך קבוצה אחרת:


In [ ]:
print(italian_artists)

In [ ]:
only_turtles.issubset(italian_artists)

ולהפך – אם קבוצה מסוימת מכילה את כל איברי הקבוצה השנייה:


In [ ]:
italian_artists.issuperset(only_turtles)
דיאגרמת ון שמדגימה ששמות צבי הנינג'ה הם תת־קבוצה של אומנים איטלקים.
אפשר לראות ששטח העיגול של הצבים "נבלע" בשטח העיגול של האומנים האיטלקים.

תרגיל ביניים: מהיר ואמין

בקובץ words.txt ישנה רשימת מילים ארוכה מאוד.

  1. בנו רשימה בשם words_list מכל המילים המופיעות בקובץ.
  2. בנו קבוצה בשם words_set מכל המילים המופיעות בקובץ.
  3. בנו פונקציה בשם average_runtime למדידת ביצועים, שתעזור לכם להחליט מה מהיר יותר:
    1. חפשו 1,000 פעמים את המילה zwitterion בכל אחד ממבני הנתונים שיצרתם.
    2. הפונקציה תחזיר את הזמן ההמוצע שנמשך החיפוש במבנה הנתונים.

כמה זמן בממוצע נמשך חיפוש המילה ברשימה? ובקבוצה?
רמז: השתמשו במודול time.

אופרטורים בין קבוצות

לעיתים יהיה לנו נוח יותר להשתמש באופרטורים במקום בפעולות.
לדוגמה, הפעולה של חיתוך בין שמות הדמויות בצבי הנינג'ה לשמות האומנים האיטלקים שנעשתה כך:


In [ ]:
italian_artists.intersection(ninja_turtles_characters)

יכולה להיעשות גם כך:


In [ ]:
italian_artists & ninja_turtles_characters

השימוש באופרטורים בין קבוצות יכול לקצר את הכתיבה ולשוות לה מראה נקי יותר.
ריכזנו עבורכם אופרטורים שבהם משתמשים תדיר כשעובדים עם קבוצות:

אופרטורים בין קבוצות
שם פעולה שם הפעולה בפייתון אופרטור דוגמה
איחוד union | {1, 3, 5} | {1, 2, 3} מחזיר {1, 2, 3, 5}
חיתוך intersection & {1, 3, 5} & {1, 2, 3} מחזיר {1, 3}
הפרש difference - {1, 3, 5} - {1, 2, 3} מחזיר {5}
{1, 2, 3} - {1, 3, 5} מחזיר {2}
הפרש סימטרי symmetric_difference ^ {1, 2, 3} ^ {1, 3, 5} מחזיר {2, 5}
בדיקת שייכות/הכלה issubset/issuperset <= {1, 2, 3} <= {1, 3, 5} מחזיר False
{1, 2, 3} <= {1, 2, 3, 4, 5} מחזיר True

שימו לב לכך שאופרטורים בין קבוצות לרוב יוצרים קבוצה חדשה.
במקרה שאנחנו יכולים לשנות קבוצה קיימת ולנצל את היותה של קבוצה mutable, נעדיף לבחור בדרך הפעולה הזו כדי לייעל את התוכנית שלנו.

כך, לדוגמה, הוספת איברים לקבוצה בצורה הזו:


In [ ]:
primes = {2, 3, 5, 7}
primes_to_add = {11, 13, 17, 19}
primes.update(primes_to_add)
print(primes)

עדיפה על הצורה הזו:


In [ ]:
primes = {2, 3, 5, 7}
primes_to_add = {11, 13, 17, 19}
primes = primes | primes_to_add
print(primes)

תרגיל לדוגמה

כתבו פונקציה שמקבלת נתיב ל־2 תיקיות, ומחזירה שמות של קבצים שמופיעים בשתי התיקיות.


In [ ]:
import os


def get_filenames(path):
    for file in os.scandir(path):
        yield file.name


def common_filenames(path1, path2):
    path1_files = set(get_filenames(path1))
    path2_files = set(get_filenames(path2))
    return path1_files & path2_files


common_filenames('images', 'resources/week5_images')

תרגילים

חזרת

כתבו פונקציה בשם uniquify שמקבלת רשימה של איברים, ומחזירה רשימה של אותם איברים וללא כפילויות.
הניחו שאיברי כל הרשימה הם immutable.

ספירת מלאי

כתבו פונקציה בשם count_specials שמקבלת מספר שלם חיובי $n$.
הפונקציה תחזיר את מספר המספרים החיוביים הנמוכים מ־$n$, שמתחלקים ב־3 או ב־7 ללא שארית.
לדוגמה, עבור $n=22$, המספרים הם: 3, 6, 7, 9, 12, 14, 15, 18 ו־21. במקרה כזה הפונקציה תחזיר 9.

ודאו שאתם משתמשים ב־set בפתרון התרגיל.

שטוחלנדיה

בפריסת מקלדת סטנדרטית ישנן 3 שורות של מקשי אותיות.
האותיות שנמצאות בפינת המקלדת השמאלית־עליונה מרכיבות את הצירוף qwerty.
מבין שמות כל המדינות בארצות הברית, ישנו רק שם מדינה אחד שאפשר לכתוב בעזרת שורה אחת בלבד במקלדת.

קראו את שמות כל המדינות בארצות־הברית מהקובץ resources/states.txt.
כתבו פונקציה בשם find_special_state.
הפונקציה תחזיר את שם המדינה שאפשר להרכיב בעזרת האותיות המופיעות באותה השורה במקלדת.

לדוגמה, potter או hash הן מילים שנכתבו בעזרת שורה אחת במקלדת.
turtle נכתבה בעזרת 2 שורות, ו־ninja נכתבה בעזרת 3 שורות.