מה זה generator בפייתון - איך מייצרים ערכים בלי לשמור הכל בזיכרון¶
בואו נדבר בכנות. יש רגע בלמידה של פייתון שבו אתם כותבים פונקציה שמחזירה רשימה ענקית, מריצים אותה, והמחשב פשוט נתקע לכמה שניות או זורק שגיאת זיכרון. שם בדיוק המקום שבו כדאי להכיר generator - כלי שמייצר ערכים אחד אחד, בדיוק כשצריך אותם, במקום לבנות הכל מראש.
הבעיה שרשימות רגילות יוצרות¶
נניח שאתם רוצים פונקציה שמחזירה את כל המספרים הזוגיים עד מספר נתון:
def even_numbers(limit):
result = []
for n in range(limit):
if n % 2 == 0:
result.append(n)
return result
הפונקציה הזו עובדת, אבל היא בונה רשימה שלמה בזיכרון לפני שהיא מחזירה משהו. אם limit הוא מספר ענק, הרשימה תופסת המון זיכרון, גם אם בפועל אתם רק רוצים לעבור על הערכים אחד אחד ולעבד אותם.
מילת המפתח yield¶
גנרטור פותר את זה בעזרת מילת מפתח אחת - yield במקום return. ברגע שפונקציה מכילה yield, פייתון מתייחס אליה אחרת לגמרי - היא לא מחזירה ערך ומסיימת, אלא "מקפיאה" את עצמה בכל yield ומחכה שיבקשו ממנה את הערך הבא:
כשקוראים ל-even_numbers(10), שום דבר לא רץ עדיין. מקבלים בחזרה אובייקט גנרטור, ורק כשמתחילים לעבור עליו, בלולאת for או עם next(), הקוד בתוך הפונקציה מתחיל לרוץ, עוצר ב-yield, מחזיר ערך אחד, וממתין לבקשה הבאה:
ההבדל המרכזי - בגרסה עם return כל הרשימה קיימת בזיכרון בבת אחת. בגרסה עם yield, בכל רגע נתון קיים בזיכרון רק הערך הנוכחי. זה נקרא הערכה עצלה - lazy evaluation - הערכים נוצרים רק כשבאמת מבקשים אותם.
רצף אינסופי כדוגמה¶
בגלל שגנרטור לא בונה הכל מראש, אפשר אפילו לכתוב אחד שרץ לנצח, וזה לא בעיה כל עוד לא מנסים להפוך אותו לרשימה:
אי אפשר לעשות list(counter()) - זה ירוץ לנצח וימלא את הזיכרון. אבל אפשר לגמרי לעבור עליו עם לולאה ולעצור מתי שרוצים:
זה בדיוק המקום שבו רשימה רגילה פשוט לא הייתה יכולה לעבוד.
הביטוי הגנרטור - generator expression¶
אם כבר הכרתם רשימות הבנה, יש לכם קיצור דרך להכיר גנרטור בלי לכתוב פונקציה שלמה עם yield - מספיק להחליף את הסוגריים המרובעים בסוגריים עגולים:
זה לא רשימה, זה גנרטור. הוא לא מחשב אף ריבוע עד שמבקשים ממנו בפועל, בדיוק כמו פונקציה עם yield. זה שימושי במיוחד כשמעבירים אותו ישירות לפונקציה שמצפה לאיטרבל, כמו sum או max, בלי לבזבז זיכרון על רשימת ביניים:
מתי כדאי להשתמש בגנרטורים¶
גנרטורים לא תמיד עדיפים על רשימה. אם אתם צריכים לעבור על הנתונים כמה פעמים, או לגשת לאיבר באמצע לפי אינדקס, רשימה רגילה מתאימה יותר - גנרטור אפשר לעבור עליו רק פעם אחת, ואז הוא "נגמר".
איפה שגנרטורים באמת נוצצים - כשעובדים עם כמויות נתונים גדולות מאוד, כשקוראים קובץ ענק שורה אחרי שורה, כשמדובר ברצף שאין לו סוף מוגדר, או כשבונים שרשרת עיבוד שבה כל שלב מזרים נתונים לשלב הבא בלי לשמור הכל בזיכרון בין לבין. אם אתם רק מתחילים ורוצים לחזק את הבסיס של לולאות ופונקציות לפני שקופצים לגנרטורים, זה בדיוק מה שמכוסה בקורס התכנות הבסיסי שלנו.
לסיכום¶
גנרטור הוא לא סוג נתונים חדש שצריך לשנן, אלא דרך חשיבה שונה - במקום לחשוב "בואו נבנה קודם את כל התוצאות", חושבים "בואו נייצר תוצאה אחת בכל פעם שמבקשים אותה". ברגע שתתקלו בקוד שאוכל יותר מדי זיכרון או לוקח יותר מדי זמן לפני שמדפיס משהו, שאלו את עצמכם אם yield יכול לפתור את זה.