בעיה א'

Image

הוראות המופיעות בגוף של מבנה 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)]

לפתרון בקוד לחצו כאן

פתרון מפורט

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

def deleteStrings(lst): 

. . . 

return lst 

 

myLst = [3, ‘x’, -17, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]

resultLst = deleteStrings(myLst)

print(resultLst) 

print(myLst) 

>>>

[3, -17, [5, ‘ABBA’], 6.3, (8, 3, 5)]

[3, -17, [5, ‘ABBA’], 6.3, (8, 3, 5)]

 

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

# No return statement 

def deleteStrings(lst): 

. . .  

 

myLst = [3, ‘x’, -17, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]

deleteStrings(myLst)

print(myLst) 

>>>

[3, -17, [5, ‘ABBA’], 6.3, (8, 3, 5)]

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

def deleteStrings(lst): 

lst = lst[:]

. . . 

return lst

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

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

def deleteStrings(lst):

    lst = lst[:]

    for item in lst: 

        # if item is a string 

            # remove item from lst  

    return lst

הקוד בגוף הפונקציה מתחיל בסריקה של הרשימה lst ערך-ערך. עבור כל ערך ברשימה, עלינו לזהות אם הוא מחרוזת, כדי לדעת אם יש למחקו. לצורך כך נשתמש בפונקציה isinstance (אפשר להשתמש גם בפונקציה type). הנה הקוד שוב, הפעם בתוספת הבדיקה –

def deleteStrings(lst):

    lst = lst[:]

    for item in lst: 

        if isinstance(item, str)

            # remove item from lst  

    return lst

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

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

def deleteStrings(lst):

    lst = lst[:]

    for item in lst: 

        if isinstance(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-. אם כן עתה יש ברשימה lst שתי מחרוזות שיש למחקן, והן מופיעות ברשימה ברצף. הנה הפלט שיתקבל מזימון הפונקציה על רשימה זו – 

print(deleteStrings([3, ‘x’, ‘y’, [5, ‘ABBA’], 6.3, ‘z’, (8, 3, 5)]))

>>>

[3, ‘y’, [5, ‘ABBA’], 6.3, (8, 3, 5)]

הרשימה המוצגת בפלט אינה הרשימה הנדרשת: מופיעה בה מחרוזת, ספציפית המחרוזת ‘y’ שהופיעה ברשימת הקלט. מדוע? הסיבה שבסריקת אוסף בלולאת for רשימת האינדקסים שיש לסרוק מוכנת בשלב ההגדרה של הלולאה, כלומר בשורה הראשונה שלה. במקרה שלפנינו, נקבע מראש שייסרקו אינדקסים 0, 1, 2, 3, 4, 5 ו-6. ההתקדמות מאינדקס לאינדקס אינה מושפעת כלל משינוי של האוסף. לכן כאן, כשהסריקה מגיעה לערך באינדקס 1 (‘x’), על אף שהוא נמחק והמחיקה משנה את מערכת האינדקסים של הרשימה הנסרקת – עכשיו היא – 0, 1, 2, 3, 4, 5 – סדרת האינדקסים שנקבע שיש לסרוק אינה משתנה, והלולאה עוברת לסרוק את הערך באינדקס 2 – ברשימה שלאחר המחיקה ערך זה הוא הרשימה המכילה את 5 ו-‘ABBA’. כך נוצר למעשה דילוג על הערך באינדקס 1 ברשימה שנוצרה בעקבות המחיקה, כלומר ‘y’, וממילא ערך זה לא נמחק. 

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

def deleteStrings(lst):

    lst = lst[:]

    for item in lst: 

        if isinstance(item, str)

            del lst[lst.index(item)]

    return lst

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

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’ לולאת ה-for אינה עוברת לסרוק את המחרוזת ‘y’ אלא את הרשימה [5, ‘ABBA’], וכך שוב מתבצע דילוג מעל מחרוזת שיש למחוק. 

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

def deleteStrings(lst):

    lst = lst[:]

    for i in range(len(lst)): 

        if isinstance(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)]

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

—-> 4     if isinstance(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)]

על אף שהרשימה קוצרה, והערך שהופיע ברשימה המקורית באינדקס 2 – כלומר המספר 17- – הוא עכשיו באינדקס 1, בסיבוב השלישי של הסריקה האינדקס הנסרק אינו 1 אלא 2, לפי סדר הסריקה שנקבע מראש, וכל הגישות הבאות לרשימה הן לרשימה שהתקבלה לאחר המחיקה או המחיקות. כלומר מכאן נמשכה הלולאה כך – 

• בסיבוב השלישי: i == 2 , lst[2] == [5, ‘ABBA’], ולא מתבצעת מחיקה.

• בסיבוב הרביעי: 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):

    lst = lst[:]

    newLst = [] 

    for item in lst: 

        if not isinstance(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 isinstance(item, str)]

גישה שניה לפתרון הבעיה היא שימוש בלולאת while, למשל כך –

def deleteStrings(lst):

    lst = lst[:]

    i = 0

    while i < len(lst): 

        item = lst[i]

        if isinstance(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)]

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

פתרון שלישי ואחרון הוא זה –

def deleteStrings(lst):

    lst = lst[:]

    for i in range(len(lst) – 1, -1, -1): 

        if isinstance(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, ואין מתבצעת מחיקה.