מילונים – חלק שני

הקדמה

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

פעולות על מילונים

ערכים ומפתחות

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


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

הערך שהפעולה מחזירה הוא iterable, ולכן קל לרוץ עליו בעזרת לולאה:


In [ ]:
for animal in loved_animals.values():
    print(animal)

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


In [ ]:
for animal_owner in loved_animals:
    print(animal_owner)

מילת המפתח del

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

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


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

אפשר להשתמש במילת המפתח del גם כדי למחוק תאים מרשימה או למחוק משתנים, אף על פי שהשימושים הללו נדירים למדי.

הפעולה pop

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


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

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

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

הפעולה update

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


In [ ]:
powers_of_two = {1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64}
high_powers_of_two = {8: 64, 9: 81, 10: 100, 11: 121}

אם נרצה למזג את high_powers_of_two לתוך powers_of_two, נצטרך לכתוב את הקוד הבא:


In [ ]:
for key, value in high_powers_of_two.items():
    powers_of_two[key] = value
    
print(powers_of_two)

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


In [ ]:
powers_of_two = {1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64}
high_powers_of_two = {8: 64, 9: 81, 10: 100, 11: 121}

In [ ]:
powers_of_two.update(high_powers_of_two)
print(powers_of_two)

מה יקרה אם ישנו מפתח שנמצא בשני המילונים?
בנו מקרה שכזה ונסו בעצמכם.

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

מבנים מורכבים נפוצים

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

רשימת מילונים

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


In [ ]:
users = [
    {'name': 'Scarlett Johansson', 'age': 34, 'country': 'United States'},
    {'name': 'Guido Van Rossum', 'age': 63, 'country': 'Netherlands', 'spouse': 'Kim Knapp'},
    {'name': 'George R. R. Martin', 'age': 70, 'country': 'United States', 'spouse': 'Parris McBride'},
]

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

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


In [ ]:
for user in users:
    print(user['name'])

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


In [ ]:
for user in users:
    print(user['spouse'])

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


In [ ]:
for user in users:
    if 'spouse' in user:
        print(f"{user['name']}'s spouse is {user['spouse']}.")
    else:
        print(f"{user['name']} has no spouse.")

או ש"נרמה" קצת בעזרת הפעולה get:


In [ ]:
for user in users:
    spouse = user.get('spouse', 'unknown')
    print(f"{user['name']}'s spouse is {spouse}.")

ערכים מרובים במפתח אחד

מילון שערכיו הם רשימות

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

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

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

נסכם לפנינו את הדרישות:

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

עבור הדרישה הראשונה, נראה שהמבנה המתאים ביותר הוא מילון שמקשר את שם המחבר לרשימת הספרים שלו.
עבור הדרישה השנייה, נראה שהמבנה המתאים ביותר הוא רשימה – סוג גמיש שקל לערוך את האיברים שבו (לעומת tuple, נניח).

נבנה מילון שכזה לדוגמה:


In [ ]:
library = {
    'Jane Austen': ['Pride and Prejudice', 'Sense and Sensibility', 'Mansfield Park', 'Emma'],
    'Mark Zusak': ['The Book Thief', 'The Messenger'],
    'Eric Arthur Blair': ['1984', 'Animal Farm'], 
    'Margaret Munnerlyn Mitchell': ['Gone with the Wind'],
    'Douglas Adams': ['The Hitchhiker\'s Guide to the Galaxy', 'The Restaurant at the End of the Universe',
                      'Life, the Universe and Everything', 'So Long, and Thanks for All the Fish', 'Mostly Harmless',
                      'And Another Thing'],
    'Khaled Hosseini': ['The Kite Runner', 'A Thousand Splendid Suns', 'And the Mountains Echoed'],
    'Harper Lee': ['To kill a mockingbird', 'Go Set a Watchman'],
}

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


In [ ]:
def view_books(library, author_name):
    return library.get(author_name)

def add_book(library, author_name, book_name):
    authors_books = library[author_name]
    authors_books.append(book_name)

def remove_book(library, author_name, book_name):
    authors_books = library[author_name]
    authors_books.remove(book_name)

add_book(library, 'Douglas Adams', 'The Salmon of Doubt')
remove_book(library, 'Douglas Adams', 'And Another Thing')
print(view_books(library, 'Douglas Adams'))

מילון שערכיו הם מילונים

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

  • תאריך ההוצאה לאור
  • רשימת הסוגות
  • כמות במלאי

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


In [ ]:
library = {
    'Jane Austen': {
        'Pride and Prejudice': {'released': '1813-01-28', 'genres': ['Classic Regency novel'], 'stock': 12},
        'Sense and Sensibility': {'released': '1813-01-28', 'genres': ['Romance novel'], 'stock': 5},
        'Mansfield Park': {'released': '1813-01-28', 'genres': ['Bildungsroman'], 'stock': 6},
        'Emma': {'released': '1813-01-28', 'genres': ['Novel of manners'], 'stock': 19},
    },
    'Markus Zusak': {
        'The Book Thief': {'released': '1813-01-28', 'genres': ['Novel-Historical Fiction'], 'stock': 4},
        'The Messenger': {'released': '1813-01-28', 'genres': ['Fiction'], 'stock': 0},
    },
    'Eric Arthur Blair': {
        '1984': {'released': '1813-01-28', 'genres': ['Dystopian', 'Political fiction', 'Social science fiction'], 'stock': 1},
        'Animal Farm': {'released': '1813-01-28', 'genres': ['Political satire'], 'stock': 7},
    },
}

In [ ]:
library = {
    'Jane Austen': [
        {'name': 'Pride and Prejudice', 'released': '1813-01-28', 'genres': ['Classic Regency novel'], 'stock': 12},
        {'name': 'Sense and Sensibility', 'released': '1813-01-28', 'genres': ['Romance novel'], 'stock': 5},
        {'name': 'Mansfield Park', 'released': '1813-01-28', 'genres': ['Bildungsroman'], 'stock': 6},
        {'name': 'Emma', 'released': '1813-01-28', 'genres': ['Novel of manners'], 'stock': 19},
    ],
    'Markus Zusak': [
        {'name': 'The Book Thief', 'released': '1813-01-28', 'genres': ['Novel-Historical Fiction'], 'stock': 4},
        {'name': 'The Messenger', 'released': '1813-01-28', 'genres': ['Fiction'], 'stock': 0},
    ],
    'Eric Arthur Blair': [
        {'name': '1984', 'released': '1813-01-28', 'genres': ['Dystopian', 'Political fiction', 'Social science fiction'], 'stock': 1},
        {'name': 'Animal Farm', 'released': '1813-01-28', 'genres': ['Political satire'], 'stock': 7},
    ],
}

In [ ]:
library = [
    {'author': 'Jane Austen', 'book': 'Pride and Prejudice', 'released': '1813-01-28', 'genres': ['Classic Regency novel'], 'stock': 12},
    {'author': 'Jane Austen', 'book': 'Sense and Sensibility', 'released': '1813-01-28', 'genres': ['Romance novel'], 'stock': 5},
    {'author': 'Jane Austen', 'book': 'Mansfield Park', 'released': '1813-01-28', 'genres': ['Bildungsroman'], 'stock': 6},
    {'author': 'Jane Austen', 'book': 'Emma', 'released': '1813-01-28', 'genres': ['Novel of manners'], 'stock': 19},
    {'author': 'Markus Zusak', 'book': 'The Book Thief', 'released': '1813-01-28', 'genres': ['Novel-Historical Fiction'], 'stock': 4},
    {'author': 'Markus Zusak', 'book': 'The Messenger', 'released': '1813-01-28', 'genres': ['Fiction'], 'stock': 0},
    {'author': 'Eric Arthur Blair', 'book': '1984', 'released': '1813-01-28', 'genres': ['Dystopian', 'Political fiction', 'Social science fiction'], 'stock': 1},
    {'author': 'Eric Arthur Blair', 'book': 'Animal Farm', 'released': '1813-01-28', 'genres': ['Political satire'], 'stock': 7},
]

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

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

תרגילים

מילים של אבן ספיר בהווה

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

לדוגמה, עבור הרשימה: [{'a': 1}, {'b': 2, 'c': 3}, {'d': 4}]
החזירו: {'a': 1, 'b': 2, 'c': 3, 'd': 4}

עוד לבנה בחומה

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

$$grade = \frac{{average(tests)}\cdot80}{100} + \frac{{average(homework)}\cdot20}{100}$$
  • ניק
    • ציוני מבחנים: 80, 78, 90, 64
    • ציוני שיעורי בית: 46, 99, 85, 90, 100
  • ריצ'ארד
    • ציוני מבחנים: 90, 92, 87, 99
    • ציוני שיעורי בית: 88, 77, 94, 66, 96
  • סיד
    • ציוני מבחנים: 66, 6, 66, 6
    • ציוני שיעורי בית: 100, 100, 100, 100, 100
  • דוד
    • ציוני מבחנים: 96, 92, 91, 78
    • ציוני שיעורי בית: 80, 77, 74, 71, 68

לדוגמה, הציון של דוד יחושב כך:

ממוצע ציוני המבחנים ($\frac{96+92+91+78}{4}$, ששווה ל־$89.25$) במשקל של 80% ($\frac{89.25\cdot80}{100}$, ששווה ל־$71.4$).
ועוד ממוצע ציוני שיעורי הבית ($\frac{68+71+74+77+80}{5}$, ששווה ל־$74$) במשקל של 20% ($\frac{74\cdot20}{100}$, ששווה ל־$14.8$).