פרק ששי

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

(1) מבוא

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

(2) רשימת מראי המקום

הדגמת ההוראות שנִלמד בפרק זה תעשה בזיקה לרשימה מסוימת אחת. רשימה זו מייצגת מפתח (Index) של פסוקי מקרא המופיע בסופו של ספר אקדמי. המפתח מופיע במבנה זה בספר: 

Gen. 15:2, 4, 8 

Gen. 16:15

Lev. 1:9, 13

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

• קיצור של שם ספר – למשל .Gen הוא קיצור של Genesis (בראשית) ו-.Lev הוא קיצור של Leviticus (ויקרא)

• מספר פרק

• פסוק או פסוקים בפרק

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

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

• הערך הראשון הוא מחרוזת, שם הספר. 

• הערך השני הוא מספר שלם, מספר הפרק. 

• הערך השלישי הוא רשימה, מספרי הפסוקים בפרק. 

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

[‘Gen.’, 15, [2, 4, 8]]

[‘Gen.’, 16, [15]] 

[‘Lev.’, 1, [9, 13]]

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

indLoc = [ [‘Gen.’, 15, [2, 4, 8]], 

           [‘Gen.’, 16, [15]], 

           [‘Lev.’, 1, [9, 13]] ]

כאן כינינו את הרשימה המייצגת את המפתח בשם indLoc.

(3) הוספת ערך לסוף רשימה – הפונקציה append

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

Gen. 15:2, 4, 8

כלומר: ספר בראשית, פרק ט”ו, פסוקים ב’, ד’ וח’. 

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

lst = [‘Gen.’]

ומטרתנו להשלים את הרשימה. 

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

lst = [‘Gen.’]

lst = lst + [15

print(lst)

>>> 

[‘Gen.’, 15]

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

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

lst = [‘Gen.’]

lst.append(15) 

print(lst) 

>>> 

[‘Gen.’, 15]

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

יש הבדל עקרוני בין הוספת ערך לרשימה באמצעות האופרטור + להוספת ערך לרשימה באמצעות הפונקציה append. נעיין שוב בקוד המשתמש באופרטור +:

lst = [‘Gen.’]

lst = lst + [15]

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

רשימה זו היא אובייקט בזכרון המחשב. שם האובייקט: lst. 

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

lst + [15]

יוצרת רשימה חדשה. רשימה זו היא אובייקט השונה מהאובייקט ששמו lst.

הרשימה החדשה מוצבת במשתנה lst במקום הרשימה הקודמת שהוצבה בו (לשם דיוק: השם lst מוקצה לרשימה החדשה וכבר לא משמש בתור כינוי לרשימה הישנה).

לעומת זאת בקוד המשתמש בפונקציה append:

lst = [‘Gen.’]

lst.append(15)

נעשות פעולות אחרות “מאחורי הקלעים”. אמנם גם הפעם בתחילת הקוד המשתנה lst מחזיק רשימה שיש בה ערך אחד, המחרוזת ‘Gen.’:

ואולם זימון הפונקציה append משנה את הרשימה במקום (in place): דהיינו lst עצמה משתנה – המספר 15 מוסף לסופה – ולא נוצרת רשימה חדשה.

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

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

lst = [‘Gen.’, 15]

נוכל להוסיף את רשימת הפסוקים בפרק ט”ו באמצעות האופרטור + כך:

lst = lst + [[2, 4, 8]] 

print(lst) 

>>> 

[‘Gen.’, 15, [2, 4, 8]]

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

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

lst = [‘Gen.’, 15]

lst.append([2, 4, 8]) 

print(lst) 

>>> 

[‘Gen.’, 15, [2, 4, 8]]

הפונקציה append מוסיפה את רשימת מספרי הפסוקים לסוף הרשימה הנתונה lst ואינה יוצרת רשימה חדשה. לכן לא כתבנו את הזימון של הפונקציה append בצד ימני של הוראת השמה. 

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

lst = [‘Ps.’, 119]

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

lst = [‘Ps.’, 150]

verses = [] 

s = input(‘Please enter verse number, q to stop: ‘)

while (s != ‘q’): 

  verses = verses + [int(s)]

  s = input(‘Please enter verse number, q to stop: ‘)

lst = lst + [verses]

הנה טבלת מעקב לתחילת הרצת הקוד עבור קלט לדוגמה – מספרי הפסוקים 1, 2, 3, 4 ו-5 (כל אחד מהם נקלט בתור מחרוזת – כלומר ‘1’, ‘2’, וכן הלאה – ומוצב במשתנה s).

פרק קי”ט בתהילים מכיל 176 פסוקים. אם המשתמשת תכניס בזה אחר זה את כל מספרי הפסוקים – מ-1 עד 176 – האופרטור + יופעל 177 פעמים (176 פעמים בתוך לולאת ה-while ופעם אחת נוספת אחריה). ואם כך יהיה, ייווצרו כאן 178 רשימות שונות זו מזו (כולל הרשימה הריקה הנוצרת בתחילת הקוד), כל אחת גדולה מהאחרת! מובן שיש כאן חוסר יעילות מבחינת שימוש במשאבי הזכרון. רצוי הרבה יותר להשתמש בפונקציה append, כך: 

lst = [‘Ps.’, 150]

s = input(‘Please enter verse number, q to stop: ‘)

verses = [] 

while (s != ‘q’): 

  verses.append(int(s)) 

  s = input(‘Please enter verse number, q to stop: ‘)

lst = lst.append([verses]) 

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

(4) הוספת ערכים לסוף רשימה – הפונקציה extend

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

Gen. 15:2, 4, 8

כלומר: ספר בראשית, פרק ט”ו, פסוקים ב’, ד’ וח’. יש בידינו שתי רשימות המשמרות כל אחת מידע חלקי על מראה המקום. ברשימה book מופיע שם הספר:

book = [‘Gen.’]

וברשימה chapAndVerses מופיעים מספר הפרק ורשימה פנימית המכילה את מספרי הפסוקים.

chapAndVerses = [15, [2, 4, 8]]

אנו רוצים לצרף את שתי הרשימות book ו-chapAndVerses לרשימה אחת, וכך ליצור את מראה המקום המלא:

[‘Gen.’, 15, [2, 4, 8]]

לכאורה נוכל להשתמש בפונקציה append כדי לצרף את chapAndVerses לסוף הרשימה book. האמנם? עיינו בקוד זה:

book.append(chapAndVerses)

print(book)

>>>

[‘Gen.’, [15, [2, 4, 8]]]

הפונקציה append טיפלה בערכים שיש ברשימה chapAndVerses בתור מכלול אחד, בתור רשימה אחת, והכניסה את כולה כמקשה אחת לסוף הרשימה book. ואולם אנו רצינו ששני הערכים ברשימה chapAndVerses – כלומר המספר 15 והרשימה [8, 4, 2] –  יוכנסו כל אחד בנפרד לסוף הרשימה book, כדי לקבל את הרשימה הזאת:

chapAndVerses = [15, [2, 4, 8]]

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

book = [‘Gen.’] 

chapAndVerses = [15, [2, 4, 8]]

book.extend(chapAndVerses)

print(book)

>>>

[‘Gen.’, 15, [2, 4, 8]] 

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

(5) הוספת ערך למקום מסוים ברשימה – הפונקציה insert

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

book = [‘Gen.’]

בהינתן רשימה זו אנו רוצים להשלימה במספר פרק ובמספרי פסוקים. 

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

chapAndVerses = [15, [2, 4, 8]]

ואנו רוצים להוסיף את שם הספר לתחילתה? נוכל להשתמש באופרטור + :

book = [‘Gen.’] 

chapAndVerses = [15, [2, 4, 8]]

fullReference = book + chapAndVerses

print(fullReference)

>>>

[‘Gen.’, 15, [2, 4, 8]]

סדר הנחת הרשימות המצורפות סביב האופרטור + – כלומר קודם book ואחר כך chapAndVerses – גרם להכנסת שם הספר לראש הרשימה החדשה שיצר האופרטור. 

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

bookAndVerses = [‘Gen.’, [2, 4, 8]]

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

bookAndVerses = [‘Gen.’, [2, 4, 8]]

chap = 15

fullReference = [bookAndVerses[0]] + [chap] + [bookAndVerses[1]]

print(fullReference)

>>>

[‘Gen.’, 15, [2, 4, 8]]

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

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

bookAndVerses = [‘Gen.’, [2, 4, 8]]

chap = 15

bookAndVerses.insert(1, chap) 

print(bookAndVerses)

>>>

[‘Gen.’, 15, [2, 4, 8]]

בתחילת הקוד מופיעה הרשימה [8, 4, 2]  באינדקס 1 ברשימה booksAndVerses.

ההוראה השנייה בקוד מכניסה את המספר 15 למקום ברשימה bookAndVerses שהאינדקס שלו הוא 1. הוספת המספר 15 גורמת לתזוזתה של הרשימה [8, 4, 2] אל המקום שהאינדקס שלו הוא 2.

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

(6) שינוי ערך אחד ברשימה – האופרטור [ ]

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

indLoc = [ [‘Gen.’, 15, [2, 4, 8]], 

           [‘Gen.’, 16, [15]], 

           [‘Lev.’, 1, [9, 13]] ]

נניח כי בעת ההכנה של רשימת מראי המקום indLoc הוכנסה לרשימות פנימיות מסוימות בה המחרוזת ‘tbc’ (קיצור של: to be completed). מחרוזת זו באה לסמן שחסר מידע ברשימה הפנימית. עד לסוף הכנת רשימת מראי המקום הוחלפו כל המחרוזות ‘tbc’ במידע החסר. כך למשל במהלך הכנת רשימה מראי המקום נוצרה רשימה זו:

[‘Gen.’, ‘tbc’, [2, 4, 8]]

המחרוזת ‘tbc’ הוכנסה לרשימה זו כדי לציין שחסר בה מספר הפרק. עד לסוף הכנת הרשימה indLoc, הוחלפה המחרוזת ‘tbc’ במספר פרק, כך:

[‘Gen.’, 15, [2, 4, 8]]

כיצד מוחלף ערך ברשימה בערך אחר? 

אפשר להשתמש באופרטור [ ]. דוגמה: 

lst = [‘Gen.’, ‘tbc’, [2, 4, 8]]

lst[1] = 15

print(lst)

>>>

[‘Gen.’, 15, [2, 4, 8]]

ההוראה הראשונה יוצרת רשימה שהמחרוזת ‘tbc’ מופיעה בה באינדקס 1.

ההוראה השנייה היא הוראת השמה. היא מורה לפייתון להציב את המספר 15 במקום מסוים ברשימה lst: המקום שהאינדקס שלו הוא 1. הערך הקודם שהיה במקום זה, ‘tbc’, נמחק.

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

lst = [‘Gen.’, 15, ‘tbc’]

lst[2] = [2, 4, 8]

print(lst)

>>>

[‘Gen.’, 15, [2, 4, 8]]

ההוראה הראשונה יוצרת רשימה שהמחרוזת ‘tbc’ מופיעה בה באינדקס 2.

ההוראה השנייה מציבה את הרשימה הזאת:

[2, 4, 8]

בתור רשימה פנימית ברשימה lst, בִמְקום הערך שהופיע ברשימה lst באינדקס 2 (כלומר המחרוזת ‘tbc’).

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

indLoc = [ [‘tbc’, 15, [2, 4, 8]], 

           [‘tbc’, 16, [15]], 

           [‘tbc’, 1, [9, 13]] ]

indLoc[0][0] = ‘Gen.’

print(indLoc) 

>>>

[[‘Gen.’, 15, [2, 4, 8]], [‘tbc’, 16, [15]], [‘tbc’, 1, [9, 13]]]

כאן ההפעלה הראשונה של האופרטור [ ] ניגשת לרשימה הפנימית באינדקס 0, וההפעלה השניה – לערך באינדקס 0 ברשימה זו.

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

indLoc = [ [‘tbc’, 15, [2, 4, 8]], 

           [‘tbc’, 16, [15]], 

           [‘tbc’, 1, [9, 13]] ]

for i in range(len(indLoc)): 

    indLoc[i][0] = ‘Gen.’

print(indLoc)  

>>> 

[[‘Gen.’, 15, [2, 4, 8]], [‘Gen.’, 16, [15]], [‘Gen.’, 1, [9, 13]]]

 

הקוד כאן סורק את רשימת מראי המקום indLoc. בכל הרשימות הפנימיות ברשימה IndLoc חסר שם של ספר, כלומר מופיעה המחרוזת ‘tbc’. הלולאה סורקת את כל הרשימות הפנימיות ומחליפה בכל אחת את הערך הראשון, כלומר המחרוזת ‘tbc’, במחרוזת שם הספר ‘.Gen’.

(7) החלפת מקטע ברשימה – האופרטור [ ]

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

lst = [‘tbc’, ‘tbc’, [2, 4, 8]]

ואנו רוצים להשלים את המידע בה, כך:

[‘Gen.’, 15, [2, 4, 8]]

נוכל לעשות זאת באמצעות שני שימושים באופרטור [ ], כך:

lst = [‘tbc’, ‘tbc’, [2, 4, 8]]

lst[0] = ‘Gen.’ 

lst[1] = 15

print(lst) 

>>> 

[‘Gen.’, 15, [2, 4, 8]]

 

בה בעת נוכל להחליף את שתי מחרוזות ה-‘tbc’ בהוראה אחת בלבד, כך:

lst = [‘tbc’, ‘tbc’, [2, 4, 8]]

lst[0:2] = [‘Gen.’, 15] 

print(lst)

>>>

[‘Gen.’, 15, [2, 4, 8]]

ההוראה הראשונה יוצרת רשימה שהמחרוזת ‘tbc’ מופיעה בה באינדקסים 0 ו-1.

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

‘Gen.’, 15

שני הערכים האלה מוצבים ברשימה lst במקום הערכים המופיעים בה באינדקסים 0 ו-1. נזכיר כי התחביר 0:2 פירושו רצף אינדקסים שאינו כולל את האינדקס 2.

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

lst = [‘tbc’, [2, 4, 8]]

lst[0:1] = [‘Gen.’, 15] 

print(lst)

>>> 

[‘Gen.’, 15, [2, 4, 8]]

ההוראה הראשונה יוצרת רשימה שיש בה מחרוזת ‘tbc’ אחת באינדקס 0.

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

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

(8) מחיקת ערך מרשימה – האופרטור del

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

lst = [‘Gen.’, 15, 17, 18, [2, 4, 8, 10]]

בשגגה נכתבו כאן, נוסף על מספר הפרק הנכון, שני מספרי פרקים מיותרים, 17 ו-18. חוץ מזה נכתב מספר פסוק מיותר: 10. האפשר למחוק ערכים מיותרים אלה מהרשימה lst, ואם אפשר – כיצד?

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

lst = [‘Gen.’, 15, 17, 18, [2, 4, 8, 10]]

del lst[2] 

print(lst) 

>>>

[‘Gen.’, 15, 18, [2, 4, 8, 10]]

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

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

del lst[2] 

print(lst) 

>>>

[‘Gen.’, 15, [2, 4, 8, 10]]

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

del lst[2][3]

print(lst)

>>>

[‘Gen.’, 15, [2, 4, 8]]

לאחר שתי פעולות המחיקה הקודמות הרשימה הפנימית, רשימת מספרי הפסוקים, הופיעה באינדקס 2 ברשימה lst. לכן בהפעלה הראשונה של האופרטור [ ] צוין האינדקס 2. בתוך רשימה פנימית זו המספר שרצינו למחוק, 10, מופיע באינדקס 3. לכן בהפעלה השנייה של האופרטור [ ] צוין האינדקס 3. הרשימה המתקבלת לאחר כל המחיקות היא תקינה. 

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

lst = [‘Gen.’, 15, [2, 4, 8]]

del lst[3] 

>>>

del lst[3]

IndexError: list assignment index out of range

כיוון שאין ברשימה lst ערך באינדקס 3, נסיון למחוק ערך מאינדקס זה הוא שגוי. 

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

lst = [‘Gen.’, 15, 17, 18, [2, 4, 8, 10]]

אם נרצה למחוק בהינף אחד את שני מספרי הפרקים המיותרים, 17 ו-18, נוכל לכתוב כך:

lst = [‘Gen.’, 15, 17, 18, [2, 4, 8, 10]]

del lst[2:4] 

print(lst) 

>>>

[‘Gen.’, 15, [2, 4, 8, 10]]

כרגיל בציון טווח בתחביר begin : end, הגבול העליון (end) הוא מספר הגדול ב-1 מהאינדקס האחרון בטווח שיש לטפל בו: כאן מחקנו את הערכים באינדקסים 2 ו-3, ואילו הערך באינדקס 4 לא נמחק.

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

lst = [‘Gen.’, 15, [2, 4, 8], [2, 4, 8]]

deletedVal = lst.pop()

print(lst)

print(deletedVal) 

>>>

[‘Gen.’, 15, [2, 4, 8]]

[2, 4, 8]

כאן לא נרחיב את הדיבור בדבר הפונקציה pop. נציין רק שיש לה ח-שיבות במימוש מבנה נתונים המכונה מחסנית (Stack).  

(9) מחיקת הופעה ראשונה של ערך ברשימה – הפונקציה remove

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

[‘Gen.’, 15, ‘tbc’, [2, 4, 8]]

בהתבוננות ברשימה הזאת אנו יכולים להבין מיד מה האינדקס שהמחרוזת ‘tbc’ מופיעה בו (2). ואולם ככלל איננו יודעים היכן בדיוק מופיעות המחרוזות ‘tbc’ המיותרות בכל רשימה פנימית, כלומר מה האינדקסים שמחרוזות אלו מופיעות בהם. לכן איננו יכולים להשתמש באופרטור del. כאן יכולה לבוא לעזרתנו הפונקציה remove. נתבונן בקוד זה:

lst = [‘Gen.’, 15, ‘tbc’, [2, 4, 8]]

lst.remove(‘tbc’)

print(lst)

>>>

[‘Gen.’, 15, [2, 4, 8]]

הפונקציה remove מקבלת ערך המופיע ברשימה ומוחקת אותו ממנה עצמה. כמו הפונקציות append, extend ו-insert, גם הפונקציה remove משנה את הרשימה שהיא מופעלת עליה במקום ואינה יוצרת רשימה חדשה שמתבטא בה השינוי המבוקש – כאן: מחיקת ערך.

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

lst = [‘Gen.’, 15, ‘tbc’, ‘tbc’ ,[2, 4, 8]]

lst.remove(‘tbc’)

print(lst)

>>>

[‘Gen.’, 15, ‘tbc’, [2, 4, 8]]

בתחילת הקוד הכילה הרשימה lst פעמיים את המחרוזת ‘tbc’ – באינדקס 2 ובאינדקס 3.

לאחר הפעלת הפונקציה remove נמחקה ההופעה הראשונה של המחרוזת ‘tbc’, זו באינדקס 2, ונשארה הופעתה השנייה. כדי למחוק אותה נוכל להפעיל את הפונקציה remove שנית. 

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

lst = [‘Gen.’, 15, [2, 4, 8]]

lst.remove(‘tbc’)

print(lst)

>>>

    lst.remove(‘tbc’)

ValueError: list.remove(x): x not in list

כאן אנו מזמנים את הפונקציה remove כדי שתמחק את המחרוזת ‘tbc’ מהרשימה lst. כיוון שמחרוזת זו אינה ברשימה, מוּצאת הודעת שגיאה.

(10) מיון רשימה במקום – הפונקציה sort

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

verses = [15, 9, 18] 

verses.sort()

print(verses) 

>>> 

[9, 15, 18]

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

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

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

verses = [15, 9, 18] 

verses.sort(reverse = True)

print(verses) 

>>>

[18, 15, 9]

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

verses = [[‘Gen.’, 16, [15]], 

          [‘Gen.’, 15, [2, 4, 8]], 

          [‘Ex.’, 1, [9, 13]] ]

verses.sort()

print(verses) 

>>>

[[‘Ex.’, 1, [9, 13]], [‘Gen.’, 15, [2, 4, 8]], [‘Gen.’, 16, [15]]]

אם עלינו למיין רשימה, ואיננו נזקקים לשמור את תכנה המקורי (לפני המיון), נעדיף להשתמש בפונקציה sort ולא בפונקציה sorted, כדי להימנע מהקצאת מקום בזכרון לרשימה שאין לנו צורך בה.

(11) הוספה לרשימה ריקה בלולאת List comprehension - for

נחתום את הדיון בפרק זה בעיון בשיטת קיצור בכתיבה של קוד היוצר רשימה ומעדכן אותה. כדי להבין במה דברים אמורים עיינו בקטע הקוד הזה ובפלט שלו. הקטע יוצר רשימה המכילה את הריבועים של עשרת המספרים השלמים הלא-שליליים הראשונים (0 עד 9).

myList = [] 

for num in range(10): 

myList.append(num ** 2) 

print(myList)

>>>

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

קוד זה ערוך לפי תבנית מסוימת, והיא זו:  

    (1) הגדרת רשימה ריקה 

    (2) הוספת ערכים לרשימה הזאת מתוך אוסף ערכים אחר – ערך אחר ערך, לפי הסדר. 

 

אפשר לקצר קוד הערוך בתבנית זו באמצעות שיטת כתיבה הנקראת List comprehension (בקיצור: LC).  הנה למשל שכתוב של קוד הדוגמה באמצעות LC: 

myList = [ num ** 2 for num in range(10) ]

ביטוי הכתוב בתבנית LC תָחוּם משני צדדיו בסוגריים מרובעים, והוא מכיל לפחות שני איברים (המופרדים זה מזה ברווח): 

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

for num in range(10)

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

0, 1, 2, 3, 4, 5, 6, 7, 8, 9

בכל סיבוב של הלולאה הערך הבא מאוסף זה מוצב במשתנה הבקרה num.

האיבר הראשון הוא ביטוי המחשב את הערכים המוכנסים לרשימה. ככלל ביטוי זה משתמש במשתנה הבקרה של לולאת ה-for. כאן הביטוי הוא:

num ** 2

הוא מחשב את הריבוע של כל ערך מהאוסף הנסרק:

0 ** 2, 1 ** 2, 2 ** 2, 3 ** 2, . . .

ולכן הוא יוצר את הסדרה הזאת:

0, 1, 4, 9, 16, 25, 36, 49, 64, 81

סדרה זו היא תכנה של הרשימה הנוצרת בסוף ביצוע הביטוי בתבנית LC. יש לקלוט את הרשימה הזאת במשתנה. בקוד הנדון הצבנו את הרשימה במשתנה myList. 

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

myList = []

נראה דוגמה אחרת מזו. הנה קוד נוסף היוצר רשימה ואינו ערוך בתבנית LC:

myList = [] 

for char in ‘car’]: 

    myList.append(char * 2)

>>>

[‘cc’, ‘aa’, ‘bb’]

בכתיבת הקוד השקול הבנוי בתבנית LC נתחיל בכתיבת הסוגריים המרובעים:

[ ] 

אחר כך נוסיף את הביטוי הסורק את אוסף הערכים שישמשו ביצירת הרשימה החדשה:

[for char in ‘car’]

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

[char * 2 for char in ‘car’]

לבסוף נקלוט במשתנה את הרשימה שנוצרה:

myList = [char * 2 for char in ‘car’]

נראה כמה דוגמות נוספות. הרשימה שיוצר הקוד הזה:

lst = [[2 , 4], [3, 5], [8, 2]]

newLst = []

for item1, item2 in lst:

    newLst.append(item1 + item2)

print(newLst) 

>>>

[6, 8, 10]

זהה לרשימה שמפיק הקוד הזה:

lst = [[2 , 4], [3, 5], [8, 2]]

newLst = [item1 + item2 for item1, item2 in lst]

print(newLst) 

>>>

[6, 8, 10]

הרשימה שיוצר הקוד הזה:

lst = [6, 8, 10]

newLst = []

for item in lst:

    newLst.append([item, item * 2])

print(newLst)

>>>

[[6, 12], [8, 16], [10, 20]]

זהה לרשימה שמפיק הקוד הזה:

lst = [6, 8, 10]

newLst = [[item, item * 2] for item in lst]

print(newLst) 

>>>

[[6, 12], [8, 16], [10, 20]]

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

lst = [2, 4, 6] 

newLst = []

for num in lst: 

    if num > 3: 

        newLst.append(num * 3) 

print(newLst) 

>>>

[12, 18] 

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

lst = [2, 4, 6]

newLst = [num * 3 for num in lst if num > 3

>>>

[12, 18]

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

lst = [2, 4, 6]

newLst = [num * 3 for num in lst if num > 3 if num < 5]  

>>>

[12]

כאן ההכנסה לרשימה מותנית בהיות המספר ברשימה הנתונה גדול מ-3 וגם קטן מ-5. נזכיר כי אפשר לכתוב את ההתניה בקיצור גם כך:

lst = [2, 4, 6]

newLst = [num * 3 for num in lst if 3 < num < 5]  

>>>

[12]

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

lst1 = [2, 4, 6]

lst2  = [4, 3, -9]

newLst = []

for num1 in lst1: 

    for num2 in lst2: 

        newLst.append(num1 * num2)

print(newLst)

>>>

[8, 6, -18, 16, 12, -36, 24, 18, -54]

עבור כל מספר num1 ברשימה lst1, נסרקים כל המספרים ברשימה lst2 בזה אחר זה, ומכפלותיהם במספר num1 מוכנסות לרשימה החדשה newLst. בקוד השקול הבנוי בתבנית LC נכתוב את שתי השורות הראשונות בלולאות בזו אחר זו, כך:

lst1 = [2, 4, 6]

lst2  = [4, 3, -9]

newLst = [num1 * num2 for num1 in lst1 for num2 in lst2 ]

print(newLst)

>>>

[8, 6, -18, 16, 12, -36, 24, 18, -54]

עתה עיינו בקוד זה:

lst1 = [2, 4, 6]

lst2  = [4, 3, -9]

newLst = []

for num1 in lst1:

    item = [] 

    for num2 in lst2: 

        item.append(num1 * num2) 

    newLst.append(item)   

print(newLst)

>>>

[[8, 6, -18], [16, 12, -36], [24, 18, -54]]

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

item = [] 

for num2 in lst2: 

    item.append(num1 * num2)

אפשר לכתוב אותו בקיצור כך:

[num1 * num2 for num2 in lst2]

אם נשלב ביטוי זה בלולאה המלאה נקבל:

newLst = []

for num1 in lst1:

    newLst.append([num1 * num2 for num2 in lst2]) 

לפי כללי כתיבת ביטוי LC יש לכתוב כך את הביטוי השקול לקוד האחרון:

newLst = [[num1 * num2 for num2 in lst2] for num1 in lst1 ]

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

myList = [] 

for num in range(10): 

    myList.append(num ** 2)

sumNums = sum(myList) 

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

sumNums = sum([num ** 2 for num in range(10)])

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

    (1) הגדרת אוסף ריק שאפשר לשנותו במקום 

    (2) הוספת ערכים לסוף האוסף הזה בלולאה, ערך אחר ערך, לפי הסדר. 

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

(12) סיכום

כל רשימה בפייתון, ולמעשה כל ערך אחר בפייתון, הוא אובייקט (object). אובייקטים הם הרכיבים היסודיים בעולמה של תכנית פייתון. כל אובייקט נועד להחזיק בערך מסוים – מספר, מחרוזת, רשימה וכו’ – וגם לטפל בערך הזה. פעמים רבות יש לאובייקטים שמות. כזה הוא למשל אובייקט הרשימה lst:

lst = [‘Gen.’, 15]

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

newLst = lst + [[2, 4, 8]]

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

del lst[1]

וכך קורה בביצוע רוב הפעולות האחרות ברשימות שנלמדו בפרק זה, ובהן הפעלת הפונקציות insert, extend, append ו-remove, הפעלת האופרטור [ ] להחלפת ערכים ברשימה, ומיון באמצעות sort. 

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

lst.append(5)

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