מילונים

הקדמה

ברשימה הבאה, כל תבליט מייצג אוסף של נתונים:

  • בחנות של האדון קשטן יש 2 בננות, 3 תפוחים ו־4 גזרים.
  • מספר הזהות של ג'ני הוא 086753092, של קווין 133713370, של איינשטיין 071091797 ושל מנחם 111111118.
  • לקווין מהסעיף הקודם יש צוללות בצבע אדום וכחול. הצוללות של ג'ני מהסעיף הקודם בצבע שחור וירוק. הצוללת שלי צהובה.
  • המחיר של פאי בחנות של קשטן הוא 3.141 ש"ח. המחיר של אווז מחמד בחנות של קשטן הוא 9.0053 ש"ח.

נסו למצוא מאפיינים משותפים לאוספים שהופיעו מעלה.

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

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

חלוקת האוספים ל־2 קבוצות של ערכים.

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

נבחן לעומק את המאפיינים המשותפים בדוגמאות שלעיל.

המשותף לאוספים
אוסף הערך הקושר (קבוצה ראשונה) הערך המתאים לו (קבוצה שנייה) הסבר
מוצרים והמלאי שלהם בחנות המוצר שנמכר בחנות המלאי מאותו מוצר יכולים להיות בחנות 5 תפוזים ו־5 תפוחים, אבל אין משמעות לחנות שיש בה 5 תפוחים וגם 3 תפוחים.
מספרי הזהות של אזרחים תעודת הזהות השם של בעל מספר הזהות יכולים להיות הרבה אזרחים העונים לשם משה לוי, ולכל אחד מהם יהיה מספר זהות שונה. לא ייתכן שמספר זהות מסוים ישויך ליותר מאדם אחד.
בעלות על צוללות צבעוניות בעל הצוללות צבע הצוללות יכול להיות שגם לקווין וגם לג'ני יש צוללות בצבעים זהים. ג'ני, קווין ואני הם אנשים ספציפיים, שאין יותר מ־1 מהם בעולם (עד שנמציא דרך לשבט אנשים).
מוצרים ומחיריהם שם המוצר מחיר המוצר לכל מוצר מחיר נקוב. עבור שני מוצרים שונים בחנות יכול להיות מחיר זהה.

מיפוי ערכים

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


In [ ]:
items = ['banana', 'apple', 'carrot']
stock = [2, 3, 4]

עבור כל תא ברשימת items, שמרנו במקום התואם ברשימת stock את הכמות שנמצאת ממנו בחנות.
יש 4 גזרים, 3 תפוחים ו־2 בננות על המדף בחנות של אדון קשטן.
שליפה של כמות המלאי עבור מוצר כלשהו בחנות תתבצע בצורה הבאה:


In [ ]:
def get_stock(item_name, items, stock):
    item_index = items.index(item_name)
    how_many = stock[item_index]
    return how_many

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


In [ ]:
print(get_stock('apple', items, stock))

צורה נוספת למימוש אותו רעיון תהיה שמירה של זוגות סדורים בתוך רשימה של tuple־ים:


In [ ]:
items = [('banana', 2), ('apple', 3), ('carrot', 4)]

ברשימה הזו הרעיון נראה מובן יותר. בואו נממש דרך לחלץ איבר מסוים מתוך הרשימה:


In [ ]:
def get_stock(item_name_to_find, items_with_stock):
    for item_to_stock in items_with_stock:
        item_name = item_to_stock[0]
        stock = item_to_stock[1]
        if item_name == item_name_to_find:
            return stock

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


In [ ]:
get_stock('apple', items)

השתמשו ב־unpacking שלמדנו במחברת הקודמת כדי לפשט את לולאת ה־for בקוד של get_stock.

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

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

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

הגדרה

מה זה מילון?

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

ישנן דוגמאות אפשריות רבות לקשרים כאלו:

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

לערך המצביע נקרא מפתח (key). זה האיבר מבין זוג האיברים שעל פיו נשמע הגיוני יותר לעשות חיפוש:

  1. העיר שאנחנו רוצים לדעת את מספר התושבים בה.
  2. הברקוד שאנחנו רוצים לדעת כמה פריטים ממנו קיימים במלאי.
  3. המילה שאת הפירושים שלה אנחנו רוצים למצוא.

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

  1. מספר התושבים בעיר.
  2. מספר הפריטים הקיימים במלאי עבור ברקוד מסוים.
  3. הפירושים של המילה במילון.

אם כך, מילון הוא בסך הכול אוסף של זוגות שכאלו: מפתחות וערכים.

הבסיס

יצירת מילון

ניצור מילון חדש:


In [ ]:
ages = {'Yam': 27, 'Methuselah': 969, 'Baby Groot': 3}

במילון הזה ישנם שלושה ערכים: הגיל של ים, של מתושלח ושל בייבי־גרוט.
המפתחות במילון הזה הם Yam (הערך הקשור למפתח הזה הוא 27), Methuselah (עם הערך 969) ו־Baby Groot (אליו הוצמד הערך 3).

יצרנו את המילון כך:

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

אפשר ליצור מילון ריק בעזרת פתיחה וסגירה של סוגריים מסולסלים:


In [ ]:
age_of_my_elephants = {}

צרו מילון עבור המלאי בחנות של אדון קשטן.

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

אחזור ערך

ניזכר כיצד מאחזרים ערך מתוך רשימה:


In [ ]:
names = ['Yam', 'Mathuselah', 'Baby Groot']

כדי לחלץ את הערך שנמצא במקום 2 ברשימה names, נכתוב:


In [ ]:
names[2]

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


In [ ]:
items = {'banana': 2, 'apple': 3, 'carrot': 4}

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


In [ ]:
items['banana']

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

ביום־יום, השתמשו במילה "בְּמָקוֹם" (b'e-ma-qom) כתחליף למילים סוגריים מרובעים.
לדוגמה: עבור שורת הקוד האחרונה, אימרו items במקום banana.

הוספה ועדכון

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


In [ ]:
items['melon'] = 1

אם הגיעו עוד 4 מלונים לחנות של אדון קשטן, נוכל לעדכן את מלאי המלונים באמצעות השמה למקום הנכון במילון:


In [ ]:
items['melon'] = items['melon'] + 4

כללי המשחק

  • לא יכולים להיות 2 מפתחות זהים במילון.
  • המפתחות במילון חייבים להיות immutables.
  • אנחנו נתייחס למילון כאל מבנה ללא סדר מסוים (אין "איבר ראשון" או "איבר אחרון").

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

סיכום ביניים

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

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

מעבר על מילון

לולאת for

כיוון שמילון הוא iterable, דרך מקובלת לעבור עליו היא באמצעות לולאת for.
ננסה להשתמש בלולאת for על מילון, ונראה מה התוצאות:


In [ ]:
favorite_animals = {'Alice': 'Cat', 'Mad hatter': 'Hare', 'Achiles': 'Tortoise'}
for something in favorite_animals:
    print(something)

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

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


In [ ]:
favorite_animals = {'Alice': 'Cat', 'Mad hatter': 'Hare', 'Achiles': 'Tortoise'}
print('favorite_animals items:')
for key in favorite_animals:
    value = favorite_animals[key]
    print(f"{key:10} -----> {value}.")  # תרגיל קטן: זהו את הטריק שגורם לזה להיראות טוב כל כך בהדפסה

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

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


In [ ]:
print(list(favorite_animals.items()))

מאחר שמדובר באיברים שבאים בזוגות, נוכל להשתמש בפירוק איברים כפי שלמדנו בשיעור על לולאות for:


In [ ]:
print('favorite_animals items:')
for key, value in favorite_animals.items():
    print(f"{key:10} -----> {value}.")

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


In [ ]:
print('favorite_animals items:')
for character, animal in favorite_animals.items():
    print(f"{character:10} -----> {animal}.")

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

מפתחות שלא קיימים

הבעיה

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


In [ ]:
empty_dict = {}
empty_dict['DannyDin']

in במילונים

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


In [ ]:
loved_animals = {'Alice': 'Cat', 'Mad hatter': 'Hare', 'Achiles': 'Tortoise'}
print('Achiles' in loved_animals)

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


In [ ]:
loved_animals = {'Alice': 'Cat', 'Mad hatter': 'Hare', 'Achiles': 'Tortoise'}

if 'Achiles' in loved_animals:
    value = loved_animals['Achiles']
else:
    value = 'Pony'

print(value)

בקוד שלמעלה, השתמשנו באופרטור ההשוואה in כדי לבדוק אם מפתח מסוים ("אכילס") קיים בתוך המילון שיצרנו בשורה הראשונה.
אם הוא נמצא שם, חילצנו את הערך שמוצמד לאותו מפתח (ל"אכילס"). אם לא, המצאנו ערך משלנו – "פוני".

מעבר על מילון יחזיר בכל חזרור מפתח מהמילון, ללא הערך הקשור אליו.
מסיבה זו, אופרטור ההשוואה in יבדוק רק אם קיים מפתח מסוים במילון, ולא יבדוק אם ערך שכזה קיים.

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

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

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


In [ ]:
def get_value(dictionary, key, default_value):
    if key in dictionary:
        return dictionary[key]
    else:
        return default_value

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

נבדוק שהפונקציה עובדת:


In [ ]:
loved_animals = {'Alice': 'Cat', 'Mad hatter': 'Hare', 'Achiles': 'Tortoise'}
print("Mad hatter: " + get_value(loved_animals, 'Mad hatter', 'Pony'))
print("Queen of hearts: " + get_value(loved_animals, 'Queen of hearts', 'Pony'))

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

הפעולה get במילונים

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


In [ ]:
loved_animals = {'Alice': 'Cat', 'Mad hatter': 'Hare', 'Achiles': 'Tortoise'}
print("Mad hatter: " + loved_animals.get('Mad hatter', 'Pony'))
print("Queen of hearts: " + loved_animals.get('Queen of hearts', 'Pony'))

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


In [ ]:
loved_animals = {'Alice': 'Cat', 'Mad hatter': 'Hare', 'Achiles': 'Tortoise'}
print(loved_animals.get('Mad hatter'))
print(loved_animals.get('Queen of hearts'))

הערך המיוחד None הוא דרך פייתונית להגיד "כלום".
אפשר לדמיין אותו כמו רִיק (וָקוּם). לא הערך המספרי אפס, לא False. פשוט כלום.

מונחים

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

תרגילים

מסר של יום טוב

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


In [ ]:
decryption_key = {
    'O': 'A', 'D': 'B', 'F': 'C', 'I': 'D', 'H': 'E',
    'G': 'F', 'L': 'G', 'C': 'H', 'K': 'I', 'Q': 'J',
    'B': 'K', 'J': 'L', 'Z': 'M', 'V': 'N', 'S': 'O',
    'R': 'P', 'M': 'Q', 'X': 'R', 'E': 'S', 'P': 'T',
    'A': 'U', 'Y': 'V', 'W': 'W', 'T': 'X', 'U': 'Y',
    'N': 'Z',
}

SONG = """
sc, kg pchxh'e svh pckvl k covl svps
pcop lhpe zh pcxsalc pch vklcp
k okv'p lsvvo is wcop k isv'p wovp ps
k'z lsvvo jkyh zu jkgh
eckvkvl jkbh o ikozsvi, xsjjkvl wkpc pch ikfh
epovikvl sv pch jhilh, k ecsw pch wkvi csw ps gju
wchv pch wsxji lhpe kv zu gofh
k eou, coyh o vkfh iou
coyh o vkfh iou
"""

מראה מראה שעל הקיר

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

  • הערכים במילון הפענוח שתיצרו הם המפתחות ממילון ההצפנה.
  • המפתחות במילון הפענוח שתיצרו הם הערכים ממילון ההצפנה.

לדוגמה, המילון {'a': '1', 'b': 2} יהפוך למילון {'1': 'a', '2': 'b'}.
השתמשו במילון הפענוח שיצרתם כדי לפענח את המסר שנשלח.


In [3]:
encryption_key = {
    'T': '1', 'F': '6', 'W': 'c', 'Y': 'h', 'B': 'k',
    'P': '~', 'H': 'q', 'S': 's', 'E': 'w', 'Q': '@',
    'U': '$', 'M': 'i', 'I': 'l', 'N': 'o', 'J': 'y',
    'Z': 'z', 'G': '!', 'L': '#', 'A': '&', 'O': '+',
    'D': ',', 'R': '-', 'C': ':', 'V': '?', 'X': '^', 
    'K': '|',
}

SONG = """
l1's ih #l6w
l1's o+c +- ow?w-
l &lo'1 !+oo& #l?w 6+-w?w-
l y$s1 c&o1 1+ #l?w cql#w l'i &#l?w
(l1's ih #l6w)
ih qw&-1 ls #l|w &o +~wo ql!qc&h
#l|w 6-&o|lw s&l,
l ,l, l1 ih c&h
l y$s1 c&o1 1+ #l?w cql#w l'i &#l?w
l1's ih #l6w
"""

פטנט, או ברונו הפיל?

בספרו המפורסם של טולסטוי "מלחמה ושלום" העלילה מתרחשת בתקופת הפלישה של נפוליאון לרוסיה.
אלכנדרוס הראשן התערב עם ברונו הפיל שהוא המציא פטנט, שבעזרתו הוא יכול למצוא את המספר שהופיע הכי הרבה פעמים בכל ספר שהוא.
השתמשו בקובץ המצורף war-and-peace.txt שנמצא בתיקיית resources, ועזרו לברונו הפיל למצוא את השנה שמוזכרת הכי הרבה פעמים בספר.