א.11 — מילונים — dict

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

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

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

פתיחת המחברת ב־Colab · השיעור וההרצאה באתר הקורס

מחברות האוניברסיטה הפתוחה — 11-מבני נתונים מתקדמים

דוגמה ראשונה: ספר טלפונים

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

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

כך כותבים את ספר הטלפונים בפייתון (המספרים בדוגמה להמחשה בלבד):

phones = {
    "Dana": "050-000-0001",
    "Noam": "050-000-0002",
    "Maya": "050-000-0003"
}

בזוג "Dana": "050-000-0001", המפתח הוא "Dana" והערך הוא "050-000-0001". המילון כולו מוקף בסוגריים מסולסלים {}, נקודתיים מפרידות בין מפתח לערך, ופסיק מפריד בין הזוגות.

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

מוצאים מספר טלפון לפי שם

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

print(phones["Dana"])
print(phones["Maya"])

פלט

050-000-0001
050-000-0003

ברשימה פונים לפי אינדקס של מיקום; במילון פונים לפי מפתח. כאן phones["Dana"] מחפשת את השם "Dana", בלי שנצטרך לדעת היכן הוא מופיע במילון.

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

דוגמה שנייה: פרטים של מכונית

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

car = {
    "model": "Toyota Corolla",
    "year": 2020,
    "license_plate": "123-45-678",
    "engine_volume": 1.8
}

המפתח "model" מציין דגם והערך שלו הוא "Toyota Corolla"; המפתח "year" מציין שנת ייצור והערך שלו הוא 2020. באותו אופן, "license_plate" מוביל למספר הרישוי לדוגמה, ו־"engine_volume" לנפח המנוע בליטרים.

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

print(car["model"])
print(car["year"])

פלט

Toyota Corolla
2020

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

עדכון ערך והוספת זוג חדש

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

phones["Dana"] = "050-000-0004"
phones["Ron"] = "050-000-0005"
print(phones["Dana"])
print(phones["Ron"])
print(len(phones))

פלט

050-000-0004
050-000-0005
4

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

בדיקת קיום והסרת זוג

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

print("Ron" in phones)
phones.pop("Ron")
print("Ron" in phones)

פלט

True
False

pop("Ron") מסירה את הזוג הכולל את השם "Ron" ואת מספר הטלפון שלו.

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

בודקים לפני ששולפים

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

name = "Dana"
if name in phones:
    print(phones[name])
else:
    print("Name not found")

פלט

050-000-0004

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

סריקה והעתקה של מילון

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

keys() נותנת תצוגה של המפתחות, values() של הערכים ו־items() של זוגות מפתח–ערך. התצוגות משקפות שינויים במילון; list(...) יוצרת מהן רשימה.

scores = {"Dana": 12, "Noam": 9}
print(list(scores.keys()))
print(list(scores.values()))
for name, score in scores.items():
    print(name, score)

saved = scores.copy()
scores["Dana"] = 20
print(saved["Dana"])

פלט

['Dana', 'Noam']
[12, 9]
Dana 12
Noam 9
12

שתי ההדפסות הראשונות מציגות את המפתחות ואת הערכים כרשימות נפרדות. לולאת for key in scores (בלי items) עוברת על המפתחות בלבד. ב־for name, score in scores.items() כל זוג נפרק לשני משתנים, כפי שלמדנו בפרק tuple. סדר הסריקה הוא סדר הכנסת המפתחות למילון, ולא מיון שלהם. השורה האחרונה בפלט מראה ש־saved["Dana"] נשאר 12 אף שהציון במילון המקורי עודכן ל־20: copy() יצרה מילון נפרד. גם במילון copy() היא העתקה שטחית, כפי שלמדנו בפרק א.10; למבנים פנימיים שצריכים העתקה נפרדת משתמשים ב־deepcopy.

מפתחות, ערכים וזוגות

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

keys() מציגה את המפתחות, values() את הערכים, ו־items() זוגות של מפתח וערך. אלו תצוגות של המילון; list הופכת אותן לרשימה נפרדת.

car = {"model": "Volvo", "year": 2020}
print(list(car.keys()))
print(list(car.values()))
print(list(car.items()))

פלט

['model', 'year']
['Volvo', 2020]
[('model', 'Volvo'), ('year', 2020)]

מילון בתוך מילון

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

workers = {
    101: {"name": "Dana", "salary": 8000},
    102: {"name": "Noam", "salary": 9000}
}
print(workers[101]["name"])
workers[101]["salary"] += 500
print(workers[101]["salary"])

פלט

Dana
8500

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