בעיה א'

הוראות המופיעות בגוף של מבנה while ומתבצעות כל עוד תנאי הלולאה מתקיים צריכות להיות מוזזות ימינה ביחס לשורה הראשונה במבנה. הקוד התקין:
age = input(‘Insert age; -1 to stop: ‘)
while age != ‘-1’:
print(age)
age = input(‘Insert age; -1 to stop: ‘)
כתבו את הפונקציה deleteStrings –
def deleteStrings(lst):
לפונקציה פרמטר אחד: lst, רשימה. הפונקציה מוחקת מהרשימה את כל המחרוזות שיש בה, ומחזירה את הרשימה שהתקבלה לאחר המחיקות (אם היו). לדוגמה הזימון הזה –
deleteStrings([3,‘x’,-17, [5,‘ABBA’], 6.3, ‘z’, (8, 3, 5)])
יחזיר רשימה זו –
[3, -17, [5, ‘ABBA’], 6.3, (8, 3, 5)]
פתרון
כדי למחוק את כל המחרוזות מהרשימה lst עלינו לאתר את כולן, וכדי לאתן את כולן עלינו לסרוק את הרשימה. מכאן שפתרון השאלה כרוך בשלוש פעולות עיקריות: סריקת הרשימה, איתור מחרוזות אגב הסריקה, ומחיקת המחרוזות שאותרו. לפי זה נציע שלד לפתרון, והוא זה –
def deleteStrings(lst):
for item in lst:
# if item is a string
# remove item from lst
return lst
הקוד בגוף הפונקציה מתחיל בסריקה של הרשימה lst ערך-ערך. עבור כל ערך ברשימה, עלינו לזהות אם הוא מחרוזת, כדי לדעת אם יש למחקו. לצורך כך נשתמש בפונקציה type (אפשר להשתמש גם בפונקציה isinstance). הנה הקוד שוב, הפעם בתוספת הבדיקה –
def deleteStrings(lst):
for item in lst:
if type(item) == str:
# remove item from lst
return lst
בשימוש בפונקציה type (ולצורך העניין גם בפונקציה isinstance) בבדיקה אם ערך הוא מסוג מסוים, עלינו לציין את שם הסוג. ככלל רצוי להכיר את שמות סוגי הערכים בפייתון. היכרות זו מסייעת בקביעת הסוג של ערכים, כמו כאן, וגם בהקשרים אחרים, למשל הבנת הודעות שגיאה.
ברגע שזיהינו שערך ברשימה lst הוא מחרוזת, עלינו למחקו מהרשימה. נבחן כמה דרכים למחיקה.
הדרך הראשונה משתמשת בפונקציה remove. הנה קוד הפונקציה המלא הפועל בדרך זו, קוד המזמן את הפונקציה, ופלט הזימון –
def deleteStrings(lst):
for item in lst:
if type(item) == str:
lst.remove(item)
return lst
print(deleteStrings([3, ‘x’, -17, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]))
>>>
[3, -17, [5, ‘ABBA’], 6.3, (8, 3, 5)]
בכל סיבוב של הלולאה העברנו לפונקציה remove את הערך הנסרק הנוכחי והיא מחקה אותו מהרשימה. המחיקה היא במקום, כלומר הערך יימחק מהרשימה lst, ולא תוחזר רשימה חדשה שבה חסר הערך שנמחק. לכן לא כתבנו את זימון הפונקציה remove בצד שמאלי של הוראת השמה, נניח כך –
newLst = lst.remove(item)
בתום הלולאה הרשימה lst אינה מכילה את כל המחרוזות שנמחקו ממנה וכפי שאנו רואים בפלט, הוחזרה הרשימה כנדרש. למראית עין הקוד נראה תקין. האמנם? הבה נריץ אותו שוב, הפעם על הרשימה הזאת –
[3, ‘x’, ‘y’, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]
רשימה זו נבדלת מרשימת הקלט הקודמת בערך יחיד: באינדקס 2 מופיעה בה המחרוזת ‘y’ ולא המספר 17-. כלומר כאן עלינו להסיר שני ערכים רצופים. הנה הפלט שיתקבל מזימון הפונקציה על רשימה זו –
print(deleteStrings([3, ‘x’, ‘x’, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]))
>>>
[3, ‘y’, [5, ‘ABBA’], 6.3, (8, 3, 5)]
הרשימה המוצגת בפלט אינה הרשימה הנדרשת: מופיעה בה מחרוזת, ספציפית המחרוזת ‘y’ שהופיעה ברשימת הקלט. מדוע? התשובה היא שבסריקת ערך-ערך ברצף באמצעות לולאת for מתבצע “מאחורי הקלעים” קוד הסורק את האינדקסים במערכת האינדקסים המוגדרת ברצף, וסריקת האינדקסים נעשית באמצעות לולאת while. במקרה שלפנינו אפשר “לתרגם” את הקוד בגוף הפונקציה לקוד המתבצע מאחורי הקלעים כך –
i = 0
while i < len(lst):
item = lst[i]
if type(item) == str:
lst.remove(item)
i += 1
בביצוע קוד זה פעולת המחיקה הראשונה מתבצעת כאשר i == 1. אז נמחקת המחרוזת הנמצאת ברשימה המקורית באינדקס 1, ומתקבלת הרשימה הזאת –
[3, ‘y’, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]
לאחר המחיקה, בסוף גוף לולאת ה-while, מיתווסף 1 לערך הנמצא במשתנה i, וכעת i == 2. אם כן בתחילת הסיבוב הבא של הלולאה, כשנבוא להציב ערך חדש במשתנה item, תבוצע ההוראה הזאת –
item = lst[2]
ברשימה שהתקבלה לאחר המחיקה הראשונה הערך באינדקס 2 אינו המחרוזת ‘y’ אלא הרשימה [5, ‘ABBA’]. הווה אומר: התרחש כאן דילוג מעל הערך שהייתה באינדקס 2 ברשימה המקורית. ממילא לא בוצעה הבדיקה מהו סוגו של ערך זה, וכיוון שכך הוא גם לא נמחק.
הבה ננסה דרך אחרת למחיקה, ונבצעה באמצעות האופרטור del. לצורך פעולתו נזקק אופרטור זה לאינדקסים. למשל אם נרצה למחוק את הערך השני ברשימה lst, זה המופיע באינדקס 1, נצטרך לכתוב כך –
del lst[1]
הנה הצעה למימוש הפונקציה deleteStrings שבו נמחקות המחרוזות באמצעות האופרטור del. בין הסוגריים המרובעים שבהוראת המחיקה הנחנו זימון של הפונקציה index: הוא יאתר את האינדקס שבה מופיע item, כלומר הערך הנסרק הנוכחי, ברשימה lst; האופרטור [ ] יקבל אינדקס זה וימחק את הערך הנמצא בו ברשימה lst.
def deleteStrings(lst):
for item in lst:
if type(item) == str:
del lst[lst.index(item)]
return lst
print(deleteStrings([3, ‘x’, -17, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]))
>>>
[3, -17, [5, ‘ABBA’], 6.3, (8, 3, 5)]
כפי שאפשר לראות, גם הפתרון הזה הצליח למחוק את כל המחרוזות מהרשימה שבה מופיע המספר 17- לאחר המחרוזת ‘x’. נבחן את פעולתו גם על הרשימה שבה מופיעה המחרוזת ‘y’ לאחר המחרוזת ‘x’ –
print(deleteStrings([3, ‘x’, ‘y’, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]))
>>>
[3, ‘y’, [5, ‘ABBA’], 6.3, (8, 3, 5)]
שוב לא קיבלנו את הרשימה הרצויה, והסיבה לכך זהה לסיבה שהוסברה למעלה: לאחר מחיקת המחרוזת ‘x’ לולאת ה-while המתבצעת מאחורי הקלעים אינה עוברת לסרוק את המחרוזת ‘y’ אלא את הרשימה [5, ‘ABBA’], וכך שוב מתבצע דילוג מעל מחרוזת שיש למחוק.
ננסה דרך שלישית, והיא סריקת אינדקסים. כלומר במקום לסרוק ערך-ערך ברשימה, נסרוק את האינדקסים במערכת האינדקסים שלה. הנה גרסה של הפונקציה הנוקטת בדרך זו –
def deleteStrings(lst):
for i in range(len(lst)):
if type(lst[i]) == str:
del lst[i]
return lst
הרצף שסורקת לולאת ה-for כאן הוא ערך החזרה של הפונקציה range –
range(len(lst))
עבור רשימות הדוגמה האלה –
[3, ‘x’, -17, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]
[3, ‘x’, ‘y’, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]
אורכה של lst הוא 7, כלומר len(lst) == 7, ולכן הרצף שסורקת לולאת ה-for הוא ערך החזרה של הזימון הזה –
range(7)
כלומר הלולאה סורקת את הסדרה 0, 1, 2, 3, 4, 5 ו-6 (כולל). זו בדיוק מערכת האינדקסים המוגדרת ברשימות הדוגמה. הוראת ה-if בודקת, עבור כל אחד מהאינדקסים הנסרקים, אם הערך המופיע ברשימה באינדקס זה הוא מחרוזת. אם זה המצב מופעל האופרטור del כדי למחוק את הערך הזה; בין הסוגריים המרובעים נכתב האינדקס שהערך מופיע בו.
נבחן את פעולת הפונקציה בגרסתה זו. שוב נתחיל ונעביר אליה את רשימת הקלט הזאת –
[3, ‘x’, -17, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]
שלא כמו הרצת שתי הגרסות הקודמות של הפונקציה על הרשימה הזאת, הפעם נקבל הודעת שגיאה –
if type(lst[i]) == str:
IndexError: list index out of range
ההודעה מציינת כי ניסינו לגשת אל אינדקס שאינו מוגדר ברשימה. הכיצד? הסיבה טמונה באופן הטיפול באינדקסים הנסרקים בלולאה. נעיין שוב בהוראה המגדירה את הסריקה –
for i in range(len(lst)):
כאשר אנו סורקים אינדקסים ברצף, רשימת האינדקסים הזאת נקבעת מראש ואינה משתנה לאורך הלולאה. כאמור עבור רשימות הדוגמה, ההוראה הזאת מגדירה שנסרוק את הסדרה 0, 1, 2, 3, 4, 5, ו-6 (כולל). בסיבוב השני של הסריקה, כאשר i == 1, זוהתה המחרוזת ‘x’ באינדקס זה, האופרטור del מחק אותה, ובסוף הסיבוב התקבלה הרשימה הזאת –
[3, -17, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]
על אף שהרשימה קוצרה, והערך שהופיע ברשימה המקורית באינדקס 3 – כלומר המספר 17- – הוא עכשיו באינדקס 2, בסיבוב השלישי של הסריקה האינדקס הנסרק אינו 2 אלא 3, לפי סדר הסריקה שנקבע מראש, וכל הגישות הבאות לרשימה הן לרשימה שהתקבלה לאחר המחיקה או המחיקות. כלומר מכאן נמשכה הלולאה כך –
• בסיבוב השלישי:
i == 3 , lst[3] == 6.3
ואין מתבצעת מחיקה.
• בסיבוב הרביעי:
i == 4 , lst[4] == ‘z’
הקוד מזהה מחרוזת, ומוחק אותה מהרשימה. אם כן בסוף הסיבוב הזה מתקבלת הרשימה הזאת –
[3, -17, [5, ‘ABBA’], 6.3, (8, 3, 5)]
וגם כאן – מחיקת ערך מהרשימה אינו משפיע כלל על סדר האינדקסים הנסרקים, אך הגישה לערכים באינדקסים אלה מתבצעת ברשימה שהתקבלה לאחר המחיקה.
• בסיבוב הרביעי: i == 5 . ברשימה lst שהתקבלה לאחר המחיקה בסיבוב הקודם אין אינדקס כזה, ומכאן החריגה שהוליכה לקטיעת התכנית ולהוצאת הודעה השגיאה.
הלקח הכללי העולה מהדיון עד כה הוא שיש להיזהר ממחיקה וכללית משינוי רשימה אגב סריקתה בלולאת for – אם סורקים ערך-ערך בה, וגם אם סורקים את האינדקסים שלה, ובמידת האפשר להשתדל להימנע מקוד כזה. נציע כאן כמה גישות חילופיות.
גישה אחת היא ליצור רצף חדש שיוכנסו אליו רק הערכים שאינם נמחקים מהרצף הנתון. הנה מימוש של הפונקציה deleteStrings לפי גישה זו –
def deleteStrings(lst):
newLst = []
for item in lst:
if type(item) != str:
newLst.append(item)
return newLst
print(deleteStrings([3, ‘x’, -17, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]))
print(deleteStrings([3, ‘x’, ‘y’, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]))
>>>
[3, -17, [5, ‘ABBA’], 6.3, (8, 3, 5)]
[3, [5, ‘ABBA’], 6.3, (8, 3, 5)]
הקוד בגוף הפונקציה מתחיל ביצירת רשימה ריקה. לאחר מכן נסרק ערך-ערך ברשימה, ואם הערך הנסרק אינו מחרוזת הוא מוכנס לסופה של הרשימה החדשה. בסוף הפונקציה מוחזרת רשימה זו, וכפי שאפשר לראות היא תקינה עבור שתי הדוגמות לרשימות קלט שנבחנו למעלה.
שימו לב: הקוד שכתבנו ערוך לפי תבנית אלגוריתמית כללית ושכיחה בתכנות, ושננקוט בה פעמים רבות בהמשך הספר. אפשר לנסחה כך –
(1) אתחול אוסף חדש מסוג רשימה, קבוצה או מילון, לאוסף ריק
(2) סריקה ערך-ערך באוסף הקיים בלולאה, על פי רוב לולאת for
(2.1) הכנסת הערך לאוסף החדש, או הכנסתו אם מתקיים תנאי כלשהו
פתרונות המממשים אלגוריתם זה בפייתון אפשר לכתוב בשיטת הקיצור Comprehension. במקרה שלפנינו, כלומר יצירת רשימה חדשה על פי רשימה קיימת, נוכל להשתמש ב-List Comprehension ולכתוב את כל גוף הפונקציה בשורה אחת כך –
def deleteStrings(lst):
return [item for item in lst if type(item) != str]
גישה שניה לפתרון הבעיה היא שימוש בלולאת while, למשל כך –
def deleteStrings(lst):
i = 0
while i < len(lst):
item = lst[i]
if type(item) == str:
lst.remove(item)
else:
i += 1
return lst
print(deleteStrings([3, ‘x’, -17, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]))
print(deleteStrings([3, ‘x’, ‘y’, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]))
>>>
[3, -17, [5, ‘ABBA’], 6.3, (8, 3, 5)]
[3, [5, ‘ABBA’], 6.3, (8, 3, 5)]
הקוד בגוף הלולאה הוא הקוד שהוצג למעלה, זה המתבצע מאחורי הקלעים בסריקת ערך ערך ברשימה בלולאת for, בהבדל אחד: כאן עברנו לאינדקס הבא ברשימה אך ורק אם בסיבוב זה לא התבצעה מחיקה. במצב זה לא מתבצע דילוג מעל הערך המופיע ברשימה לערך המחרוזת שנמחקה.
פתרון שלישי ואחרון הוא זה –
def deleteStrings(lst):
for i in range(len(lst) – 1, -1, -1):
if type(lst[i]) == str:
del lst[i]
return lst
print(deleteStrings([3, ‘x’, -17, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]))
print(deleteStrings([3, ‘x’, ‘y’, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]))
>>>
[3, -17, [5, ‘ABBA’], 6.3, (8, 3, 5)]
[3, [5, ‘ABBA’], 6.3, (8, 3, 5)]
הלולאה כאן סורקת את מערכת האינדקסים של הרשימה lst, בסדר הפוך. עבור רשימות הדוגמה נסרקת הסדרה 6, 5, 4, 3, 2, 1, ו-0 כולל. מכאן שהמחיקות נעשות מסוף הרשימה אחורה. כך לא נוצרים דילוגים מעל ערכים ברשימה. למשל עבור רשימת הקלט הזאת –
[3, ‘x’, ‘y’, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]
הזרימה היא זו –
• בסיבוב הראשון: i == 6 , lst[6] == (8, 3, 5), ואין מתבצעת מחיקה.
• בסיבוב השני:
i == 5 , lst[5] == ‘z’
הקוד מזהה מחרוזת, מוחק אותה, ומתקבלת הרשימה הזאת –
[3, ‘x’, ‘y’, [5, ‘ABBA’], 6.3, (8, 3, 5)]
• בסיבוב השלישי: i == 4 , lst[4] == 6.3, ואין מתבצעת מחיקה.
• בסיבוב הרביעי: i == 3 , lst[3] == [5, ‘ABBA’], ואין מתבצעת מחיקה.
• בסיבוב החמשי:
i == 2 , lst[2] == ‘y’
הקוד מזהה מחרוזת, מוחק אותה, ומתקבלת הרשימה הזאת –
[3, ‘x’, [5, ‘ABBA’], 6.3, (8, 3, 5)]
• בסיבוב הששי:
i == 1 , lst[1] == ‘x’
הקוד מזהה מחרוזת, מוחק אותה, ומתקבלת הרשימה הזאת –
[3, [5, ‘ABBA’], 6.3, (8, 3, 5)]
• בסיבוב השביעי והאחרון: i == 0 , lst[0] == 3, ואין מתבצעת מחיקה.
הערה חשובה אחת לסיום. שני הפתרונות האחרונים – זה המבוסס על לולאת while, וזה הסורק אינדקסים לאחור – שניהם מקבלים בתור פרמטר אוסף שאפשר לשנותו במקום – ספציפית: רשימה – ושניהם משנים במקום את האוסף הזה. השינוי הזה יכול להשתקף מחוץ לפונקציה. עיינו בקוד הזה –
myLst = [3, ‘x’, ‘y’, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]
newLst = deleteStrings(myLst)
print(newLst)
print(myLst)
>>>
[3, [5, ‘ABBA’], 6.3, (8, 3, 5)]
[3, [5, ‘ABBA’], 6.3, (8, 3, 5)]
הרשימה myLst מוגדרת מחוץ לפונקציה deleteStrings ולפני זימונה. היא מועברת לפונקציה. כפי שאפשר להיווכח מפלט הקוד, לאחר ביצוע הפונקציה משתנה הרשימה myLst והיא זהה לרשימה המוחזרת מהפונקציה. הדבר אינו מפתיע אותנו: הרי בתוך הפונקציה שינינו את הרשימה – ספציפית החלפנו ערכים בה באמצעות האופרטור [ ] – וכל השינויים היו במקום. למצב עניינים זה יש שתי השלכות חשובות. האחת היא שעקרונית כלל אין צורך להשתמש בהוראת return בסוף הפונקציה כדי לקבל את הרשימה לאחר השינוי. עיינו בקוד זה ובפלט שלו –
def deleteStrings(lst):
for i in range(len(lst) – 1, -1, -1):
if type(lst[i]) == str:
del lst[i]
myLst = [3, ‘x’, ‘y’, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]
deleteStrings(myLst)
print(newLst)
print(myLst)
>>>
[3, [5, ‘ABBA’], 6.3, (8, 3, 5)]
[3, [5, ‘ABBA’], 6.3, (8, 3, 5)]
כאן מחקנו את הוראת ההחזרה מסוף הפונקציה. לכן בקוד המופיע מחוץ לפונקציה לא שמנו את זימון הפונקציה בצד ימני של הוראת השמה – הרי היא אינה מחזירה עכשיו ערך במפורש (אמנם היא מחזירה None). אף על פי כן גם כאן השתנה תכנה של הרשימה myLst. נציין כי על אף האמור, נהוג לכתוב הוראת return בגוף הפונקציה גם במקרים מהסוג הנדון כאן, כיוון שהגדרת ערך ההחזרה של פונקציה היא רכיב עקרוני בכתיבת פונקציה.
להשפעה שיש לשינוי במקום של אוסף המועבר לפונקציה על תוכנו של האוסף מחוץ לפונקציה, יש השלכה חשובה נוספת והיא זו: אם נרצה מחוץ לפונקציה לגשת לתוכן המקורי של האוסף, יהיה עלינו לנקוט בצעדים כדי לוודא שהדבר יהיה אפשרי. מובן שאין טעם להכין עותק של אוסף לפני כל זימון של פונקציה שהוא מועבר אליה ושמשנה אותו. לכן אם חשוב לנו לגשת לתוכן האוסף גם לאחר זימון הפונקציה, נקפיד לשנות בתוך הפונקציה לא אותו כי אם עותק שלו. דרך פשוטה ליצור עותק של רשימה היא להשתמש באופרטור [ ] כפי שמתבצע בשורה הראשונה של גוף הפונקציה deleteStrings במימוש הזה שלה –
def deleteStrings(lst):
lst = lst[:]
for i in range(len(lst) – 1, -1, -1):
if type(lst[i]) == str:
del lst[i]
return lst
myLst = [3, ‘x’, ‘y’, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]
newLst = deleteStrings(myLst)
print(newLst)
print(myLst)
>>>
[3, [5, ‘ABBA’], 6.3, (8, 3, 5)]
[3, ‘x’, ‘y’, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]
ההוראה הראשונה בגוף הפונקציה –
lst = lst[:]
יוצרת עותק של הרשימה שהועברה בתור ארגומנט לפרמטר lst. הוראת ההשמה נותנת לעותק את השם lst (אפשר לתת שם אחר). מכאן ואילך כל פעולה הנעשית בגוף הפונקציה באובייקט רשימה ששמו lst נעשית על עותק של הרשימה שהועברה לפונקציה ולא על הרשימה הזאת עצמה. לכן לא ישתנה תוכן הרשימה myLst שהוגדרה מחוץ לפונקציה, כפי שאפשר להיווכח מהפלט של הוראת ההדפסה השניה בקוד.