range ושימושיו בלולאת for#
הטיפוס range#
ציינו שלולאות for מסוגלות לעבור על רשימות ועל מחרוזות. מה משותף לשני הטיפוסים?
שני הטיפוסים הללו מייצגים סדרה כלשהיא של ערכים, ומאפשרים מעבר על האיברים שלהם בזה אחר זה.
טיפוסים מסוג אלו נקראים איטרביליים (iterables).
לולאת for יודעת לעבוד בדיוק עם אובייקטים כאלה: היא “שולפת” מהם כל איבר בתורו, עד שנגמרים הערכים.
נציג כעת את range() - פונקציה המאפשרת לנו ליצור אובייקט איטרבילי נוסף: סדרה ממוינת של מספרים שלמים בתחום מסוים.
התוצאה היא אובייקט מטיפוס range שמייצר את המספרים לפי דרישה, כלומר המספרים נוצרים רק כאשר אנחנו צריכים אותם, ולא נשמרים בזיכרון כקבוצה שלמה.
זה מאוד שימושי כשמייצרים סדרות מספרים או כאשר רוצים לעבור בלולאת for על רשימות לפי אינדקסים.
תחביר#
טווח פשוט מוגדר ע”י range(stop), המייצר מספרים מאפס (כולל) עד stop (לא כולל) בקפיצות של 1. כלומר, כשאין step וגם start, מניחים שמתחילים מ0 והולכים בקפיצות של 1.
בנוסף, ניתן גם לייצר טווחים יותר מורכבים.
בדומה לslicing שראינו ברשימה, גם בrange מגדירים את תת הקבוצה המבוקשת (מתוך קבוצת המספרים השלמים) לפי start, stop ו-step:
range(start, stop, step)- מייצר מספרים מstart (כולל) עד stop (לא כולל) בקפיצות של step.range(start, stop)- מייצר מספרים מstart (כולל) עד stop (לא כולל) בקפיצות של 1. כלומר, כשאין step, מניחים שמדובר בקפיצות של 1.
כדי לראות את הערכים שrange מייצר, נצטרך להמיר אותו לרשימה.
print(list(range(10)))
print(list(range(3,10, 2)))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[3, 5, 7, 9]
שימו לב
המרת range לרשימה נעשית כאן רק לצרכי בדיקת הקוד שלנו. בד”כ, נשתדל להימנע מהמרה כזו, כיוון שהיא למעשה תשמור את כלל המספרים בזכרון.
זכרו שאחד הכוחות של range הוא ליצור את המספר הבא בסדרה לפי דרישה, ולא את כל המספרים בבת אחת.
שימו לב
בדומה לslicing ברשימות,
stepיכול להיות גם שלילי - אבל אז נצטרך שיתקייםstart>end.stepחייב להיות שונה מ0.
לולאות for על range#
בפרק הקודם ראינו איך משתמשים בrange כדי לייצג סדרה של מספרים. בדומה לאובייקטים מטיפוסים str וlist, אפשר לעבור גם על איבריו של range באמצעות לולאת for, בדיוק כפי שעשינו עם רשימות ומחרוזות. בחלק זה נראה כיצד.
הדוגמה שתלווה אותנו כאן היא הבעיה הבאה: אנחנו רוצים לחשב את סכום המספרים השלמים בין 1 לבין מיליון. חלקכם אולי יודעים שיש לזה נוסחה, וגם נציג אותה בהמשך, אבל כרגע “נשכח” ממנה כדי להדגים איך משתמשים בלולאות כדי לפתור את הבעיה.
חישוב סכום של טווח מספרים#
אילו רצינו לחשב את סכום המספרים השלמים בין 1 ל-10, זה לא היה דורש שימוש בלולאות בכלל. פשוט כותבים בפייתון:
print(1+2+3+4+5+6+7+8+9+10)
ומקבלים את התוצאה (55 אגב, למי שלא יכול לחכות…).
אבל לא סביר לפעול בגישה הזו לחישוב סכום המספרים בין 1 למיליון, אלא אם ממש משעמם לכם ואתם לעולם לא מתעייפים…
עצרו וחשבו: האם הפונקציה המובנית sum של פייתון לא מספיקה כדי לפתור את בעית סכימה המספרים מאחד למליון בקלות?
לא. אמנם הפונקציה sum של פייתון מאפשרת לחשב בפקודה אחת סכום של רשימת מספרים, אבל איך נבנה רשימה כזאת? לכתוב lst = [1, 2, 3, …, 1000000] יהיה מאוד מסורבל ולא יעיל. פתרון טוב יותר יהיה להשתמש ב-iterables (כמו range), שמייצג את המספרים מבלי לכתוב אותם במפורש ולשמור בזכרון.
כעת נדגים בקוד הבא שימוש בrange עם לולאת for.
# Simple for loop over range
for num in range(1, 3):
print(num)
1
2
תזכורת: כאשר משתמשים בrange, כמו שראינו בslicing, המספר האחרון אינו כלול בטווח, ולכן לא הודפס לנו המספר 3.
קראו את קטע הקוד שלפניכם וענו: איזה מספר יודפס בסוף הריצה?
p = 1
for num in range(1, 5):
p = p * num
print(p)
פתרון
קטע הקוד תחילה מאתחל את המשתנה p ל-1.
לאחר מכן מתבצע מעבר בלולאה על המספרים 1 עד 4 ובכל פעם כופלים את המספר הנוכחי (num) בערך הנוכחי של p, ומעדכנים את בתוצאה.
למעשה, קטע הקוד כופל המספרים 1 עד 4 וזו תוצאת ההדפסה של p. כלומר, המספר שיודפס הוא 24.
מכאן הדרך קצרה לכתיבת תוכנית שמחשבת סכום של סדרת המספרים השלמים בין 1 למליון (או 10**6). בפסאודו קוד זה ייראה כך:
הגדר משתנה סכום בשם s ואתחל אותו באפס
לכל מספר בטווח בין 1 לבין
10**6, נסמנו num:
2.1 הגדל את s ב-numהדפס את s.
ובפייתון זה ייראה כך (הריצו ובידקו מהו הסכום המתקבל):
# Simple for loop summing over range
s = 0
for num in range(1, 10**6+1):
s = s+num
print(s)
500000500000
עצרו וחשבו:
למה הגדרנו את הטווח על-ידי 10**6 + 1 ולא פשוט עד מיליון?
התוכנית הזאת עובדת מצויין. אבל דמיינו שאתם כותבים תוכנית ארוכה, ובכמה מקומות בתוכנית שלכם צריך לחשב סכום של טווח מספרים, טווח אחר בכל פעם. במקום להעתיק את 4 השורות האלו בכל פעם ולשנות את הגדרת הטווח בהתאם, אפשר להגדיר פונקציה פעם אחת ולקרוא לה בכל פעם שצריך לחשב סכום של טווח. מכיוון שהטווח עלול להיות שונה בכל פעם, אפשר להגדיר שהפונקציה תקבל שני פרמטרים – תחילת הטווח וסוף הטווח, ותחשב את הסכום בהתאם. בפייתון זה ייראה כך:
def sum_range(start, end):
s = 0
for num in range(start, end+1):
s = s+num
return s
הריצו את הקוד הבא וצפו בתוצאה:
# Executions of sum_range
def sum_range(start, end):
s = 0
for num in range(start, end+1):
s = s+num
return s
res = sum_range(1, 10**6)
print('The sum of all the integers between 1 and 1,000,000 is', res)
print('But the sum of all the integers between 3 and 3756 is', sum_range(3, 3756))
print('And the sum of all the integers between -45 and 45 is', sum_range(-45, 45))
The sum of all the integers between 1 and 1,000,000 is 500000500000
But the sum of all the integers between 3 and 3756 is 7055643
And the sum of all the integers between -45 and 45 is 0
שימו לב
הפונקציה מחזירה (return) את התוצאה, ולא מדפיסה אותה. כשקוראים לפונקציה בדרך כלל עדיף להחזיר את הערך, ולהדפיס את הערך שחזר, במקום להדפיס בתוך הפונקציה עצמה. כך ניתן יהיה להשתמש בערך שחזר גם לדבר נוספים מחוץ לפונקציה, מעבר להדפסה בלבד.
אחרי שעברנו את כל הדרך לכתיבת פונקציה שמחשבת סכום של טווח מספרים, נחזור לרגע לפונקציה המובנית sum של פייתון.
מסתבר שניתן להפעיל את sum על טווח מספרים שמוגדר על-ידי range. במילים אחרות יכולנו להחליף את הלולאה שכתבנו קודם בפקודה אחת:
def sum_range2(start, end):
return sum(range(start, end+1))
כיצד הצלחנו לחשב את הסכום הנדרש בשורה בודדת? מאחורי הקלעים, מימוש הפונקציה sum עוברת איבר-איבר על איברי הטווח ועושה חישוב מאוד דומה לחישוב שעשינו קודם באמצעות לולאה. עם זאת, לרוב, פונקציות מובנות של פייתון עובדות מהר יותר מאשר מימושים שלנו. ובאמת, אפשר לראות הבדלים ניכרים בזמן הריצה של sum_range שכתבנו קודם ל-sum_range2 הזו.
ההבדלים זניחים על טווח מספרים קטן, אבל ככל שהטווח גדול יותר ההבדלים נעשים בולטים ומשמעותיים יותר. הריצו למשל את הקוד בחלונית הבאה, שמודד זמני ריצה של חישוב הסכום של הטווח בין 1 ל-10**8, כלומר מאה מיליון. אל תתעכבו על פרטי המימוש של מדידת הזמנים כרגע. נחזור לזה בהמשך השיעור.
לאחר ההרצה שנו את הקריאה res = sum_range(1, 10**8) לקריאה res = sum_range2(1, 10**8) והריצו שוב.
# Time measurement. You can ignore this code block
import time
def timing(foo, *args):
start=time.perf_counter()
res = foo(*args)
end = time.perf_counter()
exec_time=round(end-start,2)
print(f'Execution of foo.__name__ took {exec_time} seconds. Returned value: {res}')
# Time comparison for summing over range
def sum_range(start, end):
s = 0
for num in range(start, end+1):
s = s+num
return s
def sum_range2(start, end):
return sum(range(start, end+1))
# This code timing
timing(sum_range, 1 , 10**8)
timing(sum_range2, 1 , 10**8)
Execution of foo.__name__ took 3.65 seconds. Returned value: 5000000050000000
Execution of foo.__name__ took 1.65 seconds. Returned value: 5000000050000000
מעניין לדעת! קיימת נוסחה פשוטה לחישוב סכום מהצורה הזו.
יש נוסחה פשוטה לחישוב סכום של סדרה מהצורה 1+2+…+n עבור n שלם חיובי כלשהו.
את הנוסחה גילה המתמטיקאי הנודע פרידריך גאוס, ככל הנראה בהיותו בן 6 בלבד!
למשל, לפי נוסחה זו הסכום של המספרים השלמים בין 1 ל-10 הוא:
והסכום של \(1+2+\dots+10^8\) שווה לפי נוסחה זו ל:
חשבו כיצד לממש זאת בקוד!
רמז: האם עדיף להשתמש ב/ או ב//?
חישוב כזה יעיל הרבה יותר מאשר שימוש בלולאה, ואפילו יותר מאשר שימוש בפונקציה המובנית sum של פייתון (שכאמור בעצמה עוברת על כל איברי הרשימה בלולאה).
כדי להיווכח עד כמה חישוב זה יעיל יותר, החליפו את שורת הקריאה ל-sum_range בחלונית שלמעלה במימוש החדש והריצו שוב.
אומנם התוצאה נראת דומה למדי לתוצאת ההרצה הקודמת, אבל למעשה מדובר במספר קטן הרבה יותר. רוב הסיכויים שתוצאת ההרצה תוצג כאשר בסופה האות e, אחריה מקף ואז מספר כמו 06. לדוגמה: 2.38102294921875e-06 המספר הזה הוא לא 2.38 שניות, אלא 2.38 מיליוניות השניה. בעצם הסיומת e-06 משמעותה שיש לכפול את המספר בעשר בחזקת מינוס 6, שזה 1 חלקי מיליון. מספר ממש קטן.
תרגול לולאות עם range#
צרו לולאה שמדפיסה את כל המספרים המתחלקים ב-3 מ30 (כולל) עד 102 (כולל). עליכם לממש את הקוד בשתי שורות בלבד!
# Write your code here
ממשו את הפונקציה countdown אשר המקבלת מספר שלם חיובי n, ומדפיסה ספירה לאחור מהמספר n כולל עד 1 (כולל).
עליכם לממש את תוכן הפונקציה בשתי שורות בלבד!
def countdown(n):
# Write your code here
pass
עכשיו נשים לב למשהו מאוד שימושי - כאשר כותבים range עם פרמטר יחיד בתוך הסוגריים (למשל range(100)), הטווח מתחיל ב-0, קופץ ב-1 כל פעם, והפרמטר היחיד מציין את סוף הטווח. range(100) אם כן הוא טווח המספרים בין 0 ל-99 בקפיצות של 1. כלומר 100 מספרים שונים.
מכאן, שישנה דרך מאוד פשוטה בפייתון לומר “עשה 100 פעמים את הפעולה X”:
for i in range(100):
Do X
ובאופן כללי, כדי לבצע פעולה מסויימת n פעמים:
for i in range(n):
Do X
בעצם עוברים פה על טווח המספרים 0, 1, ..., n-1 כלומר מבצעים בדיוק n איטרציות, ובכל אחת מהן מבצעים את הפעולה X. הפעולה X יכולה להשתמש בערכו של i, אבל כמובן לא חייבת. למשל:
for i in range(100):
print(i)
לעומת:
for i in range(100):
print("hello")
לסיכום - ניתן להשתמש בrange כדי לעבור על סדרות מספרים בלולאת for. המשתנה שמקבל את כל איבר בסדרה משמש בדרך כלל בתוך הלולאה, אך אין חובה להשתמש בו.
תרגול#
לפניכם קטע קוד קצר:
x = 0
y = 0
z = 0
for num in range(10, 20, 3):
if num > 5:
x = x+1
if num == 3:
y = y+1
else:
z = z+2
לפניכם קטע קוד קצר:
for i in range(10, 20, -1):
print("Hello!")
מעבר על אינדקסים של רשימה או מחרוזת#
עוד דרך שימושית להיעזר בrange בלולאות for היא לבצע איטרציה על האינדקסים של רשימה או מחרוזת נתונה. בואו נראה זאת:
לנוחיותכם חלונית עם הקוד שראינו בסרטון. מוזמנים להריץ בעצמכם:
לפניכם הקוד הבא:
def what(lst):
n = len(lst)
for i in range(n):
if lst[i] > 0:
print(i)
what([3, -1, 6, 6, -9, 0, 1])