מדריך ללימוד
עצמי של python generators.
זהו נושא מתקדם יחסית, לא מועבר ב- קורס פייתון למתחילים שלי, אלא בקורסים מתקדמים יותר.
מושג יסוד בסיסי הנדרש להבנת הטקסט:
איטרציה היא פעולה החוזרת על עצמה במהלך פתרון של בעיה, בדרך כלל בעיה כמותית. כל חזרה נקראת איטרציה. תהליך המורכב מאיטרציות קרוי תהליך איטרטיבי.
פרקי המדריך:
- לולאות for בפייתון
- איטרציות על class משלנו
- יתרונות iterator
- generator function
- generator expressions
לולאות for בפייתון
התחביר (syntax) של פייתון מאפשר הרצת לולאת for המתבססות על אובייקט שלו ממשק עם תחביר הלולאה.
נדגים זאת באמצעות אובייקט range:
>>> r1 = range(5)
>>> r1
range(0, 5)
>>> type(r1)
<class 'range'>
>>>
>>> for num in r1:
... print(num)
...
0
1
2
3
4
מתחת לפני השטח, האובייקט range חושף שיטות (methods) שבאמצעותן הוא מתקשר עם תחביר הלולאה:
>>> it = r1.__iter__()
>>> it.__next__()
0
>>> it.__next__()
1
>>> it.__next__()
2
>>> it.__next__()
3
>>> it.__next__()
4
>>> it.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
כלומר, ניתן לבקש מהאובייקט iterator (שהוא אמצעי למעבר על הערכים באובייקט).
המתודות __iter__ ו – __next__ מוכרות ע"י פייתון, ומסוכלות לתקשר עם התחביר של השפה, במקרה זה לולאות for.
איטרציות על class "שלנו"
נוכל לייצר אובייקט משלנו, שייתקשר עם התחביר של לולאות for.
יכול להיות נחמד למשל לכתוב class fibo המממש את סדרת פיבונאצ'י:
>>> for num in fibo(100):
... print(num)
...
0
1
1
2
3
5
8
13
21
34
55
89
>>>
לצורך כך, נזכור את מה שפגשנו בחלק הקודם:
- עלינו לספקת מתודת __iter__, שתחזיר הצבעה לאובייקט שבו יש מתודת __next__
כדי לפשט את הפתרון – נממש את שתי הפונקציות באותו ה – class עצמו. - בסיום האיטרציות עלינו לייצר StopIteration exception, שתפקידו לסמן ללולאה שהאיטרציות נגמרו.
להלן הדוגמא המלאה:
class fibo:
def __init__(self, limit):
self.a, self.b = 0,1
self.limit = limit
def __iter__(self):
return self
def __next__(self):
if self.limit < self.a:
raise StopIteration
ret_val = self.a
self.a, self.b = self.b, self.a + self.b
return ret_val
for num in fibo(100):
print(num)
הסבר:
- ביצירת אובייקט חדש מקבלים limit ושומרים אותו.
המטרה היא להחזיר ערכי פיבונאצ'י קטנים מ limit. - באתחול מייצרים גם שני מספרים (a ו – b ) ומאתחלים אותם.
- המתודה __iter__מחזירה גישה אל האובייקט עצמו (self) מפני שיש באובייקט עצמו מתודת __next__ כנדרש.
- חישוב הערכים נעשה ב __next__:
– אם עדיין לא חרגנו מ – limit, מחליפים את a,b לערכים הבאים שלהם, ומחזירים את a.
– אם חרגנו – מייצרים StopIteration exception.
לבדיקה – משלבים את ה class החדש בלולאת for.
יתרונות iterator
לשימוש ב iterator נוהגים לייחס מספר ייתרונות:
- חיסכון בזיכרון
נניח כי ניתבקשנו לכתוב פונקציה שמחזירה רשימה של ערכים.
אפשר להימנע מהצורך לחשב את כל הערכים, לארוז אותם ב list או tuple ולהחזיר את כולם יחד. אם במקום זאת נחזיר iterator יוכל המשתמש לעבור על הערכים, לחשב או לחפש את מה שרצה, ובעצם בכל זמן ריצת התוכנית לא יהיה רגע בו כל הערכים מאוחסנים בזיכרון. - ביצועים
אם מחזירים iterator, סימן שעדיין לא חישבנו את הערכים, ולכן הפונקציה חוזרת מהר מאד.
אם הפונקציה יכולה לחשב 1000 ערכים, אבל המשתמש מסתפק ב – 3 הראשונים, יחושבו כך רק 3 ערכים. - אפשר להחזיר אינסוף ערכים, או מספר לא ידוע מראש של ערכים..
וכו'.
פונקציית generator
פייתון שואפת להציע אפשרויות כתיבה פשוטות, מבוססות תחביר רב עוצמה, ופשוט לשימוש.
למרבה הצער, כתיבת class כאמור בסעיף הקודם, איננה פשוטה מספיק.
לכן נוצרה בפייתון טכניקה מקוצרת ליצירת iterator (או בעצם ואריאצייה שלו הנקראת generator), הלוא היא generator function.
באמצעות כתיבה פשוטה של פונציות אפשר לייצר python generators בקלות.
על המשתמש לכתוב את הקוד היוצר את רשימת הערכים, אך כאשר נוצר ערך לא מאכסנים אותו, אלא "מחזירים" את הערך באמצעות המילה השמורה yield.
האמת היא, שמאחורי הקלעים נוצר iterator בדיוק כמו במימוש המלא (עם class).
הנה הדוגמא המתאימה לפיבונאצ'י:
def fibo(limit):
a,b=0,1
while a < limit:
yield a
a,b = b,a+b
כעת נריץ כמו קודם:
>>> for num in fibo(100):
... print(num)
...
0
1
1
2
3
5
8
13
21
34
55
89
>>>
איך פועלת הדוגמא?
בירור קצר ב interpreter מבהיר את הכל.
כאשר קוראים לפונקציה, היא מחזירה generator (שהוא בעצם iterator, מגיב ל __next__ כמו iterator רגיל:
>>> gen = fibo(100)
>>> type(gen)
<class 'generator'>
>>>
>>> gen.__next__()
0
>>> gen.__next__()
1
>>> gen.__next__()
1
>>> gen.__next__()
2
>>> gen.__next__()
3
>>> gen.__next__()
5
>>> gen.__next__()
8
>>> gen.__next__()
13
>>> gen.__next__()
21
>>> gen.__next__()
34
>>> gen.__next__()
55
>>> gen.__next__()
89
>>> gen.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
לסיכום:
כאשר נידרש להחזיר בצורה יעילה שרשרת ארוכה של ערכים, עדי להחזיר iterator, וכך לחסוך מקום בזיכרון, וזמן ביצוע.
אפשר לכתוב iterator כזה בצורה פשוטה באמצעות generator function.
נותר רק כלי מרשים אחד להציג:
generator comprehension
generator expressions
רבים מכירים list comprehension בפייתון, כלי שמאפשר לייצר lists בקלות, באמצעות תחביר (syntax) רב עוצמה.
הנה דוגמא:
>>> from random import randint
>>> l1 = ['*' * i for i in range(10)]
>>> l1
['', '*', '**', '***', '****', '*****', '******', '*******', '********', '*********']
>>> for line in l1:
... print(line)
...
*
**
***
****
*****
******
*******
********
*********
אם נחליף את הסוגריים המרובעים בעגולים, לא נקבל tuple במקום list, אלא iterator ! (כלומר – ואריאצייה שלו – generator)
ביטוי כזה נקרא generator expression
גם כך אפשר לקבל python generators בקלות.
הנה:
>>> ('*' * i for i in range(10))
<generator object <genexpr> at 0x7f56f14eb270>
>>>
>>> it = ('*' * i for i in range(10))
>>>
>>> for line in it:
... print(line)
...
*
**
***
****
*****
******
*******
********
*********
קיבלנו עוד טכניקה מועילה ופשוטה ליצירת כלי רב עוצמה.
הפילוסופייה של םייתון:
עוצמה באמצעות syntax
מקווה שנהנתם.