לולאת for

הקדמה

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

  • קבל את רשימת גובהי התלמידים בכיתה, והחזר את גובהו של התלמיד הגבוה ביותר.
  • קבל את רשימת הקלפים שבידי, והחזר את הקלף המתאים ביותר לשליפה עכשיו.
  • קבל רשימת השמעה (Playlist), והחזר את כל השירים של הלהקה Led Zeppelin.
  • קבל את רשימת המסעדות בצרפת ואת הדירוגים שלהן, והחזר את 3 המסעדות בעלות הדירוג הגבוה ביותר.

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

  1. קלוט רשימת גבהים לתוך המשתנה גבהים.
  2. אפס את המשתנה ששמו הכי_גבוה_שמצאנו כך שיכיל את הערך 0.
  3. עבור כל גובה שנמצא בתוך גבהים:
    1. אם הערך של גובה גדול יותר מהכי_גבוה_שמצאנו:
      1. שים בתוך הכי_גבוה_שמצאנו את הערך של גובה.
  4. החזר את הכי_גבוה_שמצאנו.

ממשו פונקציה שמקבלת רשימת גבהים של האנשים בכיתה, ומחזירה את הגובה של התלמיד הגבוה ביותר.
לדוגמה, עבור הרשימה [1.50, 1.84, 1.73, 1.51] החזירו 1.84.

חשוב!
פתרו לפני שתמשיכו!

לולאת while

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


In [ ]:
def get_tallest(student_heights):
    max_height_so_far = 0
    current_student_index = 0
    while current_student_index < len(student_heights):
        current_student_height = student_heights[current_student_index]
        if current_student_height > max_height_so_far:
            max_height_so_far = current_student_height
        current_student_index = current_student_index + 1
    return max_height_so_far


heights = [1.50, 1.84, 1.73, 1.51]
print(get_tallest(heights))

הקוד בתא האחרון עושה את הפעולות הבאות:

  • איפוס המשתנה max_height_so_far, ששומר את הגובה המרבי שמצאנו עד כה ברשימה.
  • איפוס המשתנה current_student_index, שמצביע על מיקום התלמיד שאנחנו בודקים באיטרציה הנוכחית של הלולאה.
  • בכל איטרציה, הביטוי student_heights[current_student_index] ישיג את גובהו של אחד התלמידים, לפי הסדר.
  • אם התלמיד הנבדק גבוה יותר מהתלמיד הכי גבוה שמצאנו עד עכשיו, שמור את הגובה המרבי החדש בתוך max_height_so_far.
  • קדם את current_student_index כך שיצביע לתא שבו מופיע התלמיד הבא.
  • בסיום המעבר על כל הערכים, החזר את max_height_so_far.

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

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

שימוש ב־for

שימוש בסיסי

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


In [ ]:
names_of_students_in_class = ['Galia', 'Hadas', 'Hen', 'Ilana', 'Ivria', 'Karin', 'Maya', 'Noa']

In [ ]:
student_index = 0
while student_index < len(names_of_students_in_class):
    student_name = names_of_students_in_class[student_index]
    print(student_name)
    student_index = student_index + 1

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

נכתוב בפייתון, הפעם בעזרת לולאת for:


In [ ]:
for student_name in names_of_students_in_class:
    print(student_name)

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

איך זה עובד?

לולאת ה־for שראיתם מתחלקת ל־3 חלקים:

  1. iterable כלשהו – נסתכל על כל האיברים שבו לפי הסדר שלהם.
  2. שם משתנה חדש שנמציא – פייתון תיצור אותו במיוחד עבור הלולאה. המשתנה הזה יצביע בכל פעם על איבר אחד, לפי הסדר, מתוך ה־iterable.
  3. הפעולה או הפעולות שאנחנו רוצים לבצע על כל אחד מהאיברים האלו.

In [ ]:
names_of_students_in_class = ['Galia', 'Hadas', 'Hen', 'Ilana', 'Ivria', 'Karin', 'Maya', 'Noa']

#   השם שנמציא
#       V          Iterable, ערך שאפשר לפרק לכלל איבריו 
for student_name in names_of_students_in_class:
    print(student_name)  # <---- הפעולות לביצוע

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

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


In [ ]:
names_of_students_in_class = ['Galia', 'Hadas', 'Hen', 'Ilana', 'Ivria', 'Karin', 'Maya', 'Noa']
תוכן המשתנה names_of_students_in_class
0 1 2 3 4 5 6 7
"Galia" "Hadas" "Hen" "Ilana" "Ivria" "Karin" "Maya" "Noa"
-8 -7 -6 -5 -4 -3 -2 -1

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


In [ ]:
for student_name in names_of_students_in_class:  # <--- אנחנו פה
#    print(student_name)

חזרור ראשון, student_name מצביע על "Galia"
0 1 2 3 4 5 6 7
"Galia" "Hadas" "Hen" "Ilana" "Ivria" "Karin" "Maya" "Noa"
student_name -7 -6 -5 -4 -3 -2 -1

מייד לאחר מכן יודפס התוכן שאליו student_name מצביע:


In [ ]:
#for student_name in names_of_students_in_class:
    print(student_name)  # <--- אנחנו פה

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


In [ ]:
for student_name in names_of_students_in_class:  # <--- אנחנו פה
#    print(student_name)

המשתנה student_name יעבור להצביע על האיבר הבא, Hadas:

חזרור שני, student_name מצביע על "Hadas"
0 1 2 3 4 5 6 7
"Galia" "Hadas" "Hen" "Ilana" "Ivria" "Karin" "Maya" "Noa"
-8 student_name -6 -5 -4 -3 -2 -1

ושוב, נדפיס את המחרוזת ש־student_name מצביע עליה:


In [ ]:
#for student_name in names_of_students_in_class:
    print(student_name)  # <---- עכשיו אנחנו פה

כך נמשיך לבצע את הלולאה, עד שנגיע לאיבר האחרון ברשימה, התלמידה Noa:


In [ ]:
for student_name in names_of_students_in_class: # אנחנו פה, אחרי שעברנו על שמות כללל התלמידות, פרט לנועה
#    print(student_name)

חזרור אחרון, student_name מצביע על "Noa"
0 1 2 3 4 5 6 7
"Galia" "Hadas" "Hen" "Ilana" "Ivria" "Karin" "Maya" "Noa"
-8 -7 -6 -5 -4 -3 -2 student_name

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


In [ ]:
#for student_name in names_of_students_in_class:
    print(student_name)  # <---- עכשיו אנחנו פה

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

החלקים בלולאת for
החלק בלולאה איפה בקוד דוגמה
המבנה שאנחנו רוצים לעבור על כלל איבריו – חייב להיות iterable אחרי המילה in names_of_students_in_class
שם שנמציא למשתנה – בכל חזרור יכיל איבר מתוך ה־iterable אחרי המילה for, לפני המילה in student_name
תוכן הלולאה – הפעולות שנרצה לבצע על כל איבר אחרי הנקודתיים, בשורה חדשה (אחת או יותר), בהזחה print(student_name)

שנו את הקוד שכתבתם למציאת האדם הגבוה ביותר בכיתה, כך שישתמש ב־for ולא ב־while.
טיפ: השתמשו בפסאודו־קוד שהוצג לפני כן, והשוו בין ה־for לבין ה־while בדוגמה של הדפסת שמות התלמידים.

חשוב!
פתרו לפני שתמשיכו!

דוגמאות מילוליות נוספות

  • עבור כל עמוד בספר – קרא את העמוד.
  • עבור כל צלחת – שטוף אותה במים חמים, קרצף אותה היטב בסקוץ' ספוג בסבון, נגב אותה במגבת יבשה ואחסן אותה בארון.
  • בהינתן רשימה של 1,000 תלמידים, חשב את הגובה הממוצע של תלמיד.
  • בליל כל הקדושים, התחפש, צא החוצה, ועבור כל בית ברחוב: גש לדלת, צלצל בפעמון, אמור "ממתק או תעלול", קח ממתק ואמור תודה.

מתי להשתמש?

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

הבדלים בין while ל־for
נתון להשוואה לולאת for לולאת while
מה "מניע" את הלולאה? iterable שהלולאה תעבור על כל האיברים שלו ביטוי שערכו הבוליאני שקול ל־True או ל־False
מתי הלולאה מפסיקה כשהלולאה עברה על כל האיברים של ה־iterable כשמגיעים לתנאי של הפונקציה וערכו שקול ל־False
שימושים עיקריים ביצוע פעולה עבור כל ערך בסדרת ערכים, כמו איברי רשימה או תווים במחרוזת חזרה על פעולה כל עוד מצאנו שהמשימה לא הושלמה

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

כתבו 3 דוגמאות מילוליות נוספות ללולאות for.

חשוב!
פתרו לפני שתמשיכו!

תרגיל ביניים: פורלולה 1

בתחרות המרוצים "פורלולה 1", שבה משתתפות בקביעות 6 מכוניות מרוץ, אפשר להמר על הסדר שבו יגיעו המכוניות לקו הסיום.
משתתף זוכה הוא משתתף שהצליח לנחש נכונה את סדר ההגעה של המכוניות לקו הסיום, עם לא יותר מ־2 טעויות.
כתבו פונקציה שמקבלת הימור בודד ואת סדר ההגעה של המכוניות במרוץ, והחזירו אם ההימור זכה או הפסיד.

לדוגמה, במרוץ האחרון סדר ההגעה לקו הסיום היה:
[1, 2, 3, 4, 5, 6]

הנה דוגמאות להימורים של משתתפים ולתוצאתם:

  • [1, 2, 3, 4, 5, 6] – הימור זוכה (0 טעויות)
  • [2, 1, 3, 4, 5, 6] – הימור זוכה (2 טעויות)
  • [1, 2, 6, 4, 5, 3] – הימור זוכה (2 טעויות)
  • [1, 2, 4, 4, 5, 6] – הימור זוכה (טעות אחת)
  • [1, 6, 2, 4, 5, 3] – הימור מפסיד (3 טעויות)
  • [5, 3, 2, 4, 6, 1] – הימור מפסיד (5 טעויות)
  • [6, 5, 4, 3, 2, 1] – הימור מפסיד (6 טעויות)

מבנים מורכבים

הרשימה הבאה מכילה tuple־ים בגודל 2 איברים:


In [ ]:
words = [('star', 'rats'), ('wolf', 'flow'), ('racecar', 'racecar'), ('ekans', 'snake')]

כתבו לולאת for שתדפיס עבור כל זוג מחרוזות ברשימה: Flip "X" to get "Y".
לדוגמה, עבור הזוג האחרון מתוך 4 הזוגות, היא תדפיס: Flip "ekans" to get "snake".

חשוב!
פתרו לפני שתמשיכו!

כדי לפתור את התרגיל, כתבתם קוד שהשתמש במיקום של הערך הראשון (0) ושל הערך השני (1).
כך אני פתרתי אותו:


In [ ]:
for word_and_reversed_word in words:
    word = word_and_reversed_word[0]
    reversed_word = word_and_reversed_word[1]
    print(f'Flip "{word}" to get "{reversed_word}".')

נבחן דרך נוספת לפתור את התרגיל, רק שהפעם נשתמש בטריק שנקרא unpacking (או "פירוק").
כיוון שכל tuple ברשימת words מכיל בדיוק 2 איברים, נוכל לתת להם שמות כבר בראש הלולאה ולחלץ אותם מה־tuple:


In [ ]:
for word, reversed_word in words:
    print(f'Flip "{word}" to get "{reversed_word}".')

בכל חזרור של הלולאה שכתובה למעלה, ה־for יעבור על tuple בודד מתוך words, בדיוק כמו שקרה עד עכשיו.
השינוי הוא שבמקום שה־tuple יישמר כמו שהוא במשתנה בודד שהוגדר בראש הלולאה, הערכים שבו "יחולצו" למשתנים בראש הלולאה.
הטריק הזה עובד כיוון שבראש הלולאה כתבנו מספר משתנים שזהה למספר הערכים שנמצאים בכל tuple.

לפניכם תרשים המתאר את תוכן המשתנה words.
הטבלה הגדולה מייצגת את הרשימה words, וכל אחד מהתאים שבה מייצג tuple בתוך הרשימה.
בכל חזרור של ה־for המופיע למעלה, המשתנה word יקבל ערך שמסומן באדום, והמשתנה reversed_word יקבל את הערך הירוק התואם לו.

תצוגה של המשתנה words ושל צורת הפירוק שלו
0 1 2 3
0 1
"star" "rats"
-2 -1
0 1
"wolf" "flow"
-2 -1
0 1
"racecar" "racecar"
-2 -1
0 1
"ekans" "snake"
-2 -1
-4 -3 -2 -1

שינויים בתוך הלולאה

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

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

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


In [ ]:
numbers = ['a', 'b', 'c', 'd', 'e']
print(f"The reader expects {len(numbers)} iterations.")
for i in numbers:
    j = numbers.pop()
    print(i, j)

הלולאה הסתיימה מוקדם מהרגיל, כיוון שכשניסתה להגיע לתא שערכו 'd' הוא כבר לא היה שם.
קוד שכזה אינו צפוי, קשה לקריאה ויוצר תקלים. מומלץ מאוד לא לשנות את מספר האיברים ב־iterable בזמן הריצה.

פתרון אפשרי הוא ליצור עותק של הרשימה באמצעות הפעולה list.copy() ולהשתמש בו במקום:


In [ ]:
numbers = ['a', 'b', 'c', 'd', 'e']
numbers_to_pop = numbers.copy()
print(f"The reader expects {len(numbers)} iterations.")
for i in numbers:
    j = numbers_to_pop.pop()
    print(i, j)

עריכת הערכים שנמצאים ב־iterable

ננסה להכפיל כל תא ברשימה שלנו פי 2:


In [ ]:
numbers = [1, 3, 5]
print(f'This code will multiply every item in {numbers} by 2.')
print(f'The user expects:')
print(f'[{numbers[0] * 2}, {numbers[1] * 2}, {numbers[2] * 2}]')

for num in numbers:
    num = num * 2

print("The final result:")
print(numbers)

נוכל לראות שהרשימה נותרה ללא שינוי, למרות הלולאה שתכליתה היה להכפיל את איברי הרשימה פי 2.

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

בעצם הפעולה num * 2 אנחנו יוצרים ערך חדש שמאוחסן בכתובת שונה.

הפעולה num = num * 2 לא באמת "תשנה ערך בתוך num", אלא תגרום לו להצביע על ערך אחר.

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

דוגמאות נוספות

סכום רשימה


In [ ]:
def total(numbers):
    total = 0
    for number in numbers:
        total = total + number
    return total
    

print(total([1, 2, 3]))

ראשי תיבות


In [15]:
def acronym(sentence):
    acronym_word = ''
    for word in sentence.split():
        if len(word) >= 1:
            acronym_word = acronym_word + word[0]
    return acronym_word


print(acronym(''))



סכום איברים חיוביים

קלוט מהמשתמש מספרים. אם צריך, המר את הקלט כך שיהיה מסוג רשימה, ובה יהיו מספרים שלמים. סכום את האיברים הגדולים מ־0.


In [ ]:
def to_numbers(strings):
    numbers = []
    for semi_number in strings:
        if semi_number.isdecimal():
            numbers.append(int(semi_number))
    return numbers


def sum_positives(numbers):
    total = 0
    for number in numbers:
        if number > 0:
            total = total + number
    return total


user_numbers = input("Enter numbers seperated by comma: ")
stringy_numbers = user_numbers.replace(' ', '').split(',')
numbers = to_numbers(stringy_numbers)
print(sum_positives(numbers))

7 בום

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


In [ ]:
def sum_only_7th_places(numbers):
    total = 0
    for i in numbers[6::7]:
        total = total + i
    return total


print(sum_only_7th_places([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]))

תרגילים

אקרוסטיכון

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

Elizabeth it is in vain you say
Love not — thou sayest it in so sweet a way:
In vain those words from thee or L.E.L.
Zantippe's talents had enforced so well:
Ah! if that language from thy heart arise,
Breath it less gently forth — and veil thine eyes.
Endymion, recollect, when Luna tried
To cure his love — was cured of all beside —
His follie — pride — and passion — for he died.

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

שעורה תרבותית

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

לדוגמה: עבור הצעדים [(1, 5), (6, -2), (4, 3)] יוחזר שהמטמון נמצא בנקודה (11, 6).

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

גבעת ווטרשיפ

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