Menu

Generators ב-Python: yield, איטרציה עצלה וביטויי generator

איך generators מייצרים ערכים בעצלות ב-Python: המילה השמורה yield, ביטויי generator, ומתי הם עדיפים על רשימה רגילה.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

פונקציה שעוצרת באמצע

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

הפשוט ביותר שאפשר:

שימו לב ל-yield במקום return. בפעם הראשונה ש-for מבקשת ערך, Python מריצה את גוף הפונקציה עד שהיא מגיעה ל-yield 1. הפונקציה עוצרת בדיוק שם, מגישה את 1 בחזרה ללולאה, וזוכרת בדיוק איפה עצרה, כולל כל המשתנים. האיטרציה הבאה ממשיכה מאותה נקודה: current += 1, חזרה ל-while, yield 2. וכך הלאה, עד שתנאי הלולאה נכשל, ואז ה-generator פשוט מפסיק.

העצירה וההמשך האלה הם כל הטריק.

למה לא פשוט לבנות רשימה?

כי גרסת הרשימה מקצה את כל הערכים מראש:

בשביל 5 פריטים זה בסדר. עכשיו דמיינו שאתם רוצים 50 מיליון מספרים שלמים, ואכפת לכם רק מהראשון שעומד בתנאי כלשהו. גרסת הרשימה מקצה 50 מיליון מספרים ואז זורקים את רובם. גרסת ה-generator יוצרת בדיוק כמה שהקורא צורך. כשלולאת ה-for מוצאת את מה שהיא רוצה ויוצאת עם break, ה-generator פשוט מפסיק.

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

ביטויי generator

אם כתבתם פעם list comprehension, אתם כבר מכירים את התחביר: פשוט מחליפים את הסוגריים המרובעים בסוגריים עגולים:

squares_gen עוד לא חישב כלום. הוא רק מתכון. מעבר עליו מריץ את המתכון צעד אחרי צעד.

ביטויי generator מושלמים כארגומנטים לפונקציות שצורכות iterable:

אין רשימת ביניים. sum, max ו-any קוראות ערכים אחד בכל פעם, וזה בדיוק מה שהן רוצות.

קריאת קובץ גדול, שורה אחרי שורה

זה המקרה הקלאסי מהעולם האמיתי ל-generators: עיבוד קובץ גדול מדי כדי לטעון אותו לזיכרון:

def parse_log_lines(path):
    with open(path) as f:
        for line in f:
            if line.startswith("ERROR"):
                yield line.rstrip()

for error in parse_log_lines("app.log"):
    print(error)

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

פעם אחת וזהו

ל-generator יש מעבר יחיד. אחרי שעברתם עליו עד הסוף, הוא מתרוקן:

הלולאה השנייה לא מדפיסה כלום. ל-generator לא נשאר כלום.

אם צריך לעבור יותר מפעם אחת, או שקוראים שוב לפונקציית ה-generator כדי לקבל generator חדש, או שהופכים את הרצף לרשימה עם list(...) ועוברים על הרשימה שוב ושוב. בחרו לפי העלות: בנייה מחדש מתאימה אם העבודה זולה; רשימה מתאימה אם הרצף קטן.

next() ואיטרציה ידנית

לא חייבים להשתמש בלולאת for. next() שולפת ערך אחד בכל פעם:

StopIteration היא הדרך של generator לאותת "סיימתי". לולאות for תופסות אותה בשקט. בקוד ידני אפשר להעביר ברירת מחדל ל-next(gen, default) כדי להימנע מהחריגה.

generators אינסופיים

מאחר שהערכים מיוצרים לפי דרישה, generator יכול לייצג רצף שאין לו סוף, כל עוד הצרכן מפסיק לבקש:

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

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

yield from: האצלה ל-iterable אחר

אם ה-generator שלכם רוצה להניב כל ערך מתוך iterable אחר, yield from עושה את זה בשורה אחת:

בלי yield from הייתם כותבים לולאת for מקוננת עם yield x בפנים. הוא גם מעביר כראוי קריאות ל-send() ול-throw() אם תשתמשו בהן פעם, אבל בקוד יומיומי חשבו עליו כעל "להניב כל ערך מהדבר הזה".

מתי לפנות ל-generator

שלושה סימנים לכך ש-generator הוא הכלי הנכון:

  1. הרצף גדול, אולי אינסופי, או יקר לייצור במלואו.
  2. הצרכן עשוי לעצור לפני הסוף (למשל break בהתאמה הראשונה).
  3. אתם רוצים לשרשר טרנספורמציות (סינון, מיפוי, לקיחה) בלי לבנות רשימות ביניים.

ומתי לא:

  • צריך גישה אקראית (seq[42]). generators הולכים רק קדימה.
  • צריך לעבור על אותו רצף כמה פעמים. השתמשו ברשימה.
  • הרצף קטן וכבר יש לכם אותו. list comprehension פשוט יותר.

generators, list comprehensions ורשימות רגילות הם כל אחד התשובה הנכונה לעבודה אחרת. המיומנות היא לבחור אחד בלי לחשוב על זה יותר מדי, והדרך המהירה ביותר לפתח את האינסטינקט הזה היא לשים לב, בכל איטרציה שאתם כותבים, אם "לייצר הכל קודם" או "לייצר אחד בכל פעם" מתאים יותר.

הבא בתור: Context Managers לעומק

ראיתם עכשיו את רוב הביטויים שבהם Python משתמשת לאיטרציה. Context managers, הצהרת with, מגיעים עכשיו, והם משתלבים היטב עם generators להזרמת נתונים מקבצים ומחיבורי רשת.

שאלות נפוצות

מה זה generator ב-Python?

generator הוא פונקציה שמייצרת ערכים אחד בכל פעם, ועוצרת ביניהם. כותבים אותו עם def כמו פונקציה רגילה, אבל משתמשים ב-yield במקום ב-return. קריאה לו מחזירה אובייקט generator; כל איטרציה של for או כל קריאה ל-next() מריצה את הפונקציה עד ה-yield הבא.

מה ההבדל בין רשימה ל-generator?

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

אפשר לעבור על generator פעמיים?

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

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל