פרק תשיעי

מילון

תוכן העניינים

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

age = input(‘Insert age; -1 to stop: ‘)

while age != ‘-1’:

    print(age)

    age = input(‘Insert age; -1 to stop: ‘)

 

(1) מבוא

נשוב ונעיין בנתונים לתכנית המכוניות שכתבנו בפרק הקודם. כזכור התכנית התבססה על מידע בנוגע לשתי תכונות של מכוניות שנמכרו בישראל: מספר רישוי וצבעּ. הדגמנו את פעילות התכנית באמצעות שני קבצים שהכילו חלק ממידע זה. כך לדוגמה הקובץ colors2019.txt הכיל מידע בנוגע ל-5 מכוניות שנמכרו בשנת 2019. זה תוכן הקובץ:

נוכל להסתכל על המידע השמור בקובץ colors2019.txt גם בתור מיפוי (mapping): כל מספר רישוי ממופה לשם צבע מסוים:

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

בתכנית שכתבנו שמרנו את המידע האצור בקובץ colors2019.txt בתוך רשומה של רשומות בשם licensesAndColors2019. זה תכנה:

((‘3812048’, ‘red’), (‘9852826’, ‘blue’), (‘3315724’, ‘blue’), (‘6570751’, ‘yellow’), (‘5005910’, ‘brown’) . . .)

נעיין עכשיו בבעיה זו: 

נתון מספר רישוי של מכונית המופיע בקובץ colors2019.txt.

עלינו להדפיס את צבעהּ של המכונית שזה מספר הרישוי שלה.

 

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

    (1) סריקה של כל רשומה פנימית ברשומה licensesAndColors2019

    (1.1)     אם מספר הרישוי המופיע ברשומה שווה למספר הרישוי הנתון 

    (1.1.1)            הדפסת שם הצבע 

    (1.1.2)            יציאה מהלולאה

 

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

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

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

(2) מילון – מבנה ותכונות כלליות

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

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

בפייתון מילון נכתב בתבנית זו:

{key1 : value, key2 : value, key3 : value . . . }

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

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

engFre = {‘Goodbye’:’au revoir’,

          ‘Talk’:’Parler’ }

כל אחד משני הזוגות במילון engFre הוא מיפוי. המילה’Goodbye’ ממופה לתרגום ‘au revoir’. המילה ‘Talk’ ממופה לתרגום ‘Parler’. המילים האנגליות ‘Goodbye’ ו-‘Talk’ הן מפתחות המילון engFre, ואילו המילים ‘au revoir’ ו-‘Parler’ הן הערכים במילון.

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

engFre = {‘Hello’:’Bonjour’, 

          ‘Hello’:’allô’ }

במילון זה מוגדרים שני זוגות החולקים מפתח אחד, ‘Hello’. מצב זה בלתי אפשרי במילון.

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

engFre = {‘Hello’  :[‘Bonjour’,’allô’]

          ‘Goodbye’:[‘au revoir’] }

כל אחד משני הזוגות במילון engFre הוא מיפוי. המילה’Hello’ ממופה לרשימה הכוללת שתי מילים, שתיהן תרגומים לצרפתית של המילה ‘Hello’. המילה ‘Goodbye’ ממופה לרשימה הכוללת תרגום אחד שלה לצרפתית. המילים האנגליות ‘Hello’ ו-‘GoodBye’ הן מפתחות המילון engFre, ואילו הרשימה [‘Bonjour’, ‘allô’] והרשימה [‘au revoir’] הן הערכים במילון. 

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

engFre = {‘Hello’    : [‘Bonjour’,’allô’], 

          ‘Goodbye’  : [‘au revoir’], 

          ‘Hillbilly’: 111 }

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

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

engFre = {‘Hello’    : [‘Bonjour’,’allô’], 

          ‘Goodbye’  : [‘au revoir’],

          ‘Hillbilly’: 111, 

          999        : [‘Élan’, ‘Dépayser’] }

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

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

נוכל למצוא את מספר הזוגות במילון באמצעות הפונקציה len. דוגמה: 

engFre = {‘Hello’   : [‘Bonjour’,’allô’], 

          ‘Goodbye’ : [‘au revoir’] }

print(len(engFre)) 

>>> 

2

כאן הפעלנו את הפונקציה len על המילון engFre והיא החזירה את מספר הזוגות בו, 2.

(3) יצירת מילון

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

engFre = {‘Hello’   : [‘Bonjour’,’allô’], 

          ‘Goodbye’ : [‘au revoir’] }

המילון המוגדר כאן מוצב במשתנה engFre. 

נגדיר מילון ריק כך:

emptyDict = {}

תזכורת: קבוצה ריקה מוגדרת באמצעות הפונקציה set (ראו פרק שמיני, סעיף 8). 

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

lst = [ [‘Hello’, [‘Bonjour’,’allô’] ], 

        [‘Goodbye’, [‘au revoir’] ] ]

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

engFre = dict(lst) 

print(engFre)

>>>

{‘Hello’: [‘Bonjour’, ‘allô’], ‘Goodbye’: [‘au revoir’] }

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

tup = ( [‘Hello’, [‘Bonjour’,’allô’] ], 

        [‘Goodbye’, [‘au revoir’]] )

ניתן את הרשומה tup לפונקציה dict, המילון שתיצור הפונקציה ותחזיר יהיה זהה למילון שהחזירה כשקיבלה את הרשימה lst.

(4) האם מפתח קיים במילון? האם אינו קיים?

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

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], 

          ‘Goodbye’: [‘au revoir’], 

          ‘Speak’: [‘Parler’]}

result = ‘Goodbye’ in engFre

print(result) 

>>>

True

הפעלת האופרטור in בהוראה השניה תוביל לחיפוש המילה ‘Goodbye’ באוסף המפתחות של המילון engFre. כיוון שהמילה ‘Goodbye’ היא אחת ממפתחות המילון, האופרטור in יחזיר True וערך זה יוצב במשתנה result. 

נשתמש באופרטור not in כדי לבדוק אי-קיום מפתח במילון. דוגמה: 

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], 

          ‘Goodbye’: [‘au revoir’], 

          ‘Speak’: [‘Parler’]}

result = ‘Parler’ not in engFre

print(result) 

>>>

True

גם כאן יוצב True במשתנה result: הפעם כיוון שהמילה ‘Parler’ אינה מפתח במילון engFre (אמנם היא מופיעה בתוך הרשימה שאליה ממופה מפתח במילון).

(5) הכנסת זוגות למילון קיים ועדכונם

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

engFre = {‘Hello’  :[‘Bonjour’,’allô’], 

          ‘Goodbye’:[‘au revoir’] }

engFre[‘Speak’] = [‘Parler’]

print(engFre) 

>>>

{‘Hello’: [‘Bonjour’, ‘allô’], ‘Goodbye’: [‘au revoir’], ‘Speak’: [‘Parler’]}

ההוראה השנייה היא הוראת השמה:

engFre[‘Speak’] = [‘Parler’]

והיא קובעת את הזוג שיוכנס למילון: 

המפתח בזוג הוא מה שמופיע בתוך הסוגריים המרובעים בצדה השמאלי של ההוראה, כלומר המחרוזת ‘Speak’.

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

דוגמה נוספת: 

translations = [(‘Hello’, [‘Bonjour’,’allô’]),

                (‘Goodbye’, [‘au revoir’])  ] 

engFre = {}

for eng, fre in translations: 

    engFre[eng] = fre 

print(engFre) 

>>>

{‘Hello’: [‘Bonjour’, ‘allô’], ‘Goodbye’: [‘au revoir’]}

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

    (1) הגדרת מילון ריק

    (2) עבור כל רשומה פנימית ברשימה translation

    (2.1)        הצבת זוג הערכים ברשומה (מילה ותרגומה) במשתנים eng ו-fre

    (2.2)        הכנסת הזוג eng:fre למילון

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

translations = [(‘Hello’, [‘Bonjour’,’allô’]),

                (‘Goodbye’, [‘au revoir’])  ] 

engFre = {eng : fre for eng, fre in translations}

print(engFre) 

>>>

{‘Hello’: [‘Bonjour’, ‘allô’], ‘Goodbye’: [‘au revoir’]}

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

engFre = {‘Hello’  :[‘Bonjour’,’allô’], 

          ‘Goodbye’:[‘au revoir’],

          ‘Speak’  :[‘Parler’] }

engFre [‘Speak’] = [‘Parler’, ‘Discuter’]

>>> 

{‘Hello’: [‘Bonjour’, ‘allô’], ‘Goodbye’: [‘au revoir’], ‘Speak’: [‘Parler’, ‘Discuter’]}

בדוגמה זו עדכנו את הערך שהמפתח ‘Speak’ ממופה אליו: קודם מופה מפתח זה לרשימה [‘Parler’] ואילו עתה הוא ממופה לרשימה [‘Parler’, ‘Discuter’].

(6) (אי-) סדר במילון

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

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], 

          ‘Goodbye’: [‘au revoir’], 

          ‘Speak’: [‘Parler’]}

print(engFre[1])

>>>

    print(engFre[1])

KeyError: 1

המספר 1 המופיע בהוראה הזאת:

engFre[1]

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

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

engFre = {} 

engFre[‘Hello’] = [‘Bonjour’, ‘allô’]

engFre[‘Goodbye’] = [‘au revoir’]

engFre[‘Speak’] = [‘Parler’]

print(engFre)

>>>

{‘Hello’: [‘Bonjour’, ‘allô’], ‘Goodbye’: [‘au revoir’], ‘Speak’: [‘Parler’]}

כאן אנו יכולים להניח כי סדר הזוגות במילון engFre הוא סדר הכנסתם למילון זה. 

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

(7) מפתחות, ערכים וזוגות – העתקתם וסריקתם

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

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], 

          ‘Goodbye’: [‘au revoir’], 

          ‘Speak’: [‘Parler’]}

key = ‘Speak’

print(engFre[key])

>>>

[‘Parler’]

כאן בהוראה אחת בלבד ניגשנו לערך שאליו ממופה המפתח ‘Speak’. כך עשינו בדוגמה זו שהמילון בה מכיל 3 זוגות, וכך נוכל לעשות גם במילון המייצג מילון אנגלי-צרפתי המכיל יותר מ-500000 מילים באנגלית. לעומת זאת לו היינו שומרים את זוגות המילון ברשימה של רשימות פנימיות, לא היינו יכולים לגשת לערך המבוקש אלא באמצעות לולאה, למשל כך:

engFreLst = [ [‘Hello’, [‘Bonjour’, ‘allô’] ],

              [‘Goodbye’, [‘au revoir’] ],

              [‘Speak’,   [‘Parler’] ] 

               . . . ]

key = ‘Hello’

for eng, fre in engFreLst:

    if key == eng:

        print(fre)

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

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

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], ‘Goodbye’: [‘au revoir’], ‘Speak’: [‘Parler’]}

print(engFre[‘Dinner’])

>>>

    print(engFre[‘Dinner’])

KeyError: ‘Dinner’

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

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], 

          ‘Goodbye’: [‘au revoir’], 

          ‘Speak’: [‘Parler’], 

          ‘Lire’:[‘Read’]}

val = engFre.get(‘Lire’)

print(val) 

>>>

[‘Read’]

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

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], ‘Goodbye’: [‘au revoir’], ‘Speak’: [‘Parler’], ‘Lire’:[‘Read’]}

val = engFre.get(‘Dinner’, ‘Key not found.’)

print(val) 

>>>

Key not found.

כאן נתנו לפונקציה get ארגומנט שני, המחרוזת ‘.Key not found’ . הפונקציה החזירה את המחרוזת הזאת כיוון שלא מצאה באוסף המפתחות של המילון engFre את המחרוזת ‘Dinner’. 

שימוש בפונקציה get עדיף משימוש באופרטור [ ] אם ידוע לנו שעלולה להתבצע גישה לערך שאינו מפתח במילון: במקרה כזה נימָנע מהודעת שגיאה ונחזיר ערך שיעיד על היות המפתח בלתי קיים במילון. 

 

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

באמצעות הפונקציה keys נקבל אוסף של כל המפתחות במילון:

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], ‘Goodbye’: [‘au revoir’], ‘Speak’: [‘Parler’]}

engFreKeys = engFre.keys()

print(engFreKeys) 

>>>

dict_keys([‘Hello’, ‘Goodbye’, ‘Speak’])

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

print(engFreKeys[0]) 

>>>

    print(engFreKeys[0])

TypeError: ‘dict_keys’ object does not support indexing

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

keysLst = list(engFre.keys())

print(keysLst)

>>>

[‘Hello’, ‘Goodbye’, ‘Speak’]

הקוד כאן יצר רשימה של מפתחות במילון engFre. 

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

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], 

          ‘Goodbye’: [‘au revoir’], 

          ‘Speak’: [‘Parler’]}

engFreLst = list(engFre)

print(engFreLst) 

>>>

[‘Hello’, ‘Goodbye’, ‘Speak’]

הפעלת הפונקציה list ישירות על המילון engFre הפיקה רשימה של המפתחות בו. 

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

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], 

          ‘Goodbye’: [‘au revoir’], 

          ‘Speak’: [‘Parler’]}

engFreKeys = list(engFre.keys()) 

engFreKeys.sort() 

print(engFreKeys) 

>>> 

[‘Goodbye’, ‘Hello’, ‘Speak’]

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

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], 

          ‘Goodbye’: [‘au revoir’], 

          ‘Speak’: [‘Parler’]}

engFreKeysSorted = sorted(engFre)

>>> 

[‘Goodbye’, ‘Hello’, ‘Speak’]

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

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], 

          ‘Goodbye’: [‘au revoir’], 

          ‘Speak’: [‘Parler’]}

engFreValues = engFre.values()

print(engFreValues) 

>>>

dict_values([[‘Bonjour’, ‘allô’], [‘au revoir’], [‘Parler’]])

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

valuesLst = list(engFreValues) 

print(valuesLst)

>>>

[[‘Bonjour’, ‘allô’], [‘au revoir’], [‘Parler’]]

באמצעות הפונקציה items נקבל אוסף של כל הזוגות במילון:

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], 

          ‘Goodbye’: [‘au revoir’], 

          ‘Speak’: [‘Parler’]}

engFreItems = engFre.items()

print(engFreItems)

>>>

dict_items([(‘Hello’, [‘Bonjour’, ‘allô’]), (‘Goodbye’, [‘au revoir’]), (‘Speak’, [‘Parler’])])

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

pairsLst = list(engFreItems) 

print(pairsLst) 

>>> 

[(‘Hello’, [‘Bonjour’, ‘allô’]), (‘Goodbye’, [‘au revoir’]), (‘Speak’, [‘Parler’])]

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

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], 

          ‘Goodbye’: [‘au revoir’], 

          ‘Speak’: [‘Parler’]}

for k in engFre.keys(): 

    print(k) 

>>>

Hello

Goodbye

Speak

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

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], 

          ‘Goodbye’: [‘au revoir’], 

          ‘Speak’: [‘Parler’]}

for val in engFre.values(): 

    print(val)

>>>

[‘Bonjour’, ‘allô’]

[‘au revoir’]

[‘Parler’]

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

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], 

          ‘Goodbye’: [‘au revoir’], 

          ‘Speak’: [‘Parler’]}

for k, val in engFre.items(): 

    print(k, val)

>>>

Hello [‘Bonjour’, ‘allô’]

Goodbye [‘au revoir’]

Speak [‘Parler’]

בשורת הראשונה של הלולאה כאן מבוצעת השמה מרובה: כל זוג מפתח-ערך במבנה הנתונים שמחזירה הפונקציה items מוצב במשתנים k ו-val – המפתח במשתנה k והערך במשתנה val.

(8) מחיקת זוג ממילון

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

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], 

          ‘Goodbye’: [‘au revoir’], 

          ‘Speak’: [‘Parler’],

          ‘Lire’ : [‘Read’]}

הזוג האחרון במילון הוא שגוי, כיוון שהמפתח בו הוא מילה בצרפתית והערך הוא מילה באנגלית. אי אפשר לגשת אל הזוג הזה ולשנותו כנדרש. בייחוד אי אפשר לגשת למפתח ‘Lire’ ולהחליפו ב-‘Read’, כדי לתקן נוכל למחוק את הזוג מהמילון ולהכניס זוג תקין. 

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

seasons = [‘winter’, ‘spring’, ‘summer’, ‘autumn’, ‘winter’]

del seasons[4]

print(seasons) 

>>>

[‘winter’, ‘spring’, ‘summer’, ‘autumn’]

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

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], 

                    ‘Goodbye’: [‘au revoir’], 

                    ‘Speak’: [‘Parler’], 

                    ‘Lire’:[‘Read’]}

del engFre[‘Lire’]

print(engFre) 

>>>

{‘Hello’: [‘Bonjour’, ‘allô’], ‘Goodbye’: [‘au revoir’], ‘Speak’: [‘Parler’]}

כאן הפעלנו את האופרטור del על המילון engFre כדי להסיר את הזוג שהמפתח בו הוא ‘Lire’. 

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

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], 

          ‘Goodbye’: [‘au revoir’], 

          ‘Speak’: [‘Parler’]}

del engFre[‘Dinner’]

>>>

    print(engFre[‘Dinner’])

KeyError: ‘Dinner’

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

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], 

          ‘Goodbye’: [‘au revoir’], 

          ‘Speak’: [‘Parler’], 

          ‘Lire’:[‘Read’]}

val = engFre.pop(‘Lire’)

print(engFre) 

print(val) 

>>>

[‘Read’]

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

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], 

          ‘Goodbye’: [‘au revoir’], 

          ‘Speak’: [‘Parler’], 

          ‘Lire’:[‘Read’]}

val = engFre.pop(‘Lire’)

print(engFre) 

print(val) 

>>>

[‘Read’]

 

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

engFre = {‘Hello’: [‘Bonjour’, ‘allô’], 

          ‘Goodbye’: [‘au revoir’], 

          ‘Speak’: [‘Parler’], 

          ‘Lire’:[‘Read’]}

val = engFre.pop(‘Dinner’, ‘Key not found.’)

print(val) 

>>>

Key not found.

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

(9) דוגמת מעשית – שמירת מילון אנגלי-צרפתי

הקובץ en-fr.train.txt (לחצו על שמו כדי להורידו) מכיל תרגומים של 1000 מלים באנגלית לצרפתית (הקובץ הוא גרסה מוקטנת של קובץ בשם זהה שחיבר Aman Chadha, ומופיע בחשבון ה-GitHub שלו; הקובץ נצפה לאחרונה ב-20 בנובמבר 2021 והכיל אז 10872 רשומות).  כל שורה בקובץ מכילה מילה באנגלית ותרגומה לצרפתית (מילה אחת). הנה כמה שורות בלתי רצופות בקובץ:

the le

and et

was fut

for pour

that cela

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

f = open(“en-fr.train.txt”, “r”)

s = f.read()

f.close()

lines = s.split(‘\n’)

engFre = {}

for line in lines:    

    word, trans = line.split() 

    engFre[word] = trans

 

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

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

{‘the’: ‘la’, 

 ‘and’: ‘et’, 

 ‘was’: ‘ֳétait tait’, 

 ‘for’: ‘pour’, 

 ‘that’: ‘cela’}

הבעיה מזדקרת לעין כבר בעיון בזוג הראשון: למעלה ראינו כי בקובץ הנתונים המילה האנגלית ‘the’ ממופה מילה הצרפתית ‘le’, ואילו כאן היא ממופה למילה ‘la’! כדי להבין את הבעיה נסתכל בעשר השורות הרצופות הראשונות בקובץ: 

the le

the les

the la

and et

was fut

was etait

was était

for pour

that que

that cela

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

engFre[word] = trans

ההוראה הזאת לא תטפל היטב בשורה בקובץ המכילה מילה אנגלית שכבר הוכנסה למילון. כך יקרה למשל לאחר שהוכנס למילון המידע בשורה הראשונה בקובץ, כלומר המפתח ‘the’ והערך ‘le’, ותיסרק השורה השנייה בקובץ. בסריקת שורה זו יוכנס התרגום הצרפתי ‘les’ בתור ערך של מפתח שכבר קיים במילון: המילה ‘the’. ובסריקת השורה השלישית בקובץ יעודכן שוב הערך שהמפתח ‘the’ ממופה אליו: עתה הוא יהיה ‘la’!

אם כן עולה השאלה: כיצד אפשר – 

• לשמור את המידע בקובץ בתוך מילון, באופן שכל מילה באנגלית תשמש מפתח (ולכן לא תחזור על עצמה באוסף מפתחות המילון – שלא כמו בקובץ, שיש בו מילים אנגליות המופיעות ביותר משורה אחת) 

ובה בעת 

• לשמור את כל התרגומים של מילה אנגלית שמופיעה ביותר משורה אחת בקובץ? 

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

{‘the’: [‘le’, ‘les’, ‘la’], 

 ‘and’: [‘et’], 

 ‘was’: [‘fut’, ‘etait’, ‘ֳétait’], 

 ‘for’: [‘pour’], 

 ‘that’: [‘que’, ‘cela’]}

כך ייראה הקוד המתוקן:

f = open(“en-fr.train.txt”, “r”)

s = f.read()

f.close()

lines = s.split(‘\n’)

engFre = {}

for line in lines:    

    word, trans = line.split()

    if word not in engFre: 

        engFre[word] = [trans]

    else: 

        engFre[word].append(trans)

בסריקה של כל שורה ושורה בקובץ, התכנית מַבחינה בין שני מקרים אלה: 

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

מקרה ב’ – המילה האנגלית בשורה היא כבר מפתח במילון – במקרה זה יש להוסיף את תרגום המילה לסוף רשימת התרגומים שהמפתח (המילה) ממופה אליו. 

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

(12) סיכום

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