2.31. איטרטורים וגנרטורים

לולאת ה-for עשתה יותר עבודה ממה שזה נראה. עמוד זה מכסה את פרוטוקול האיטרטור שעליו היא פועלת, ואת מילת המפתח yield המאפשרת לך לבנות איטרטורים משלך.

2.31.1. פרוטוקול האיטרטור

כל אובייקט שניתן לבצע עליו לולאה ממש שתי מתודות:

  • __iter__() – מחזירה איטרטור על פריטי האובייקט.

  • __next__() – על האיטרטור, מחזירה את הפריט הבא או מעלה StopIteration כאשר אין עוד.

ה-built-in iter() קורא ל-__iter__; next() קורא ל-__next__. צעד דרך רשימה ידנית:

it = iter([10, 20, 30])
print(next(it))    # 10
print(next(it))    # 20
print(next(it))    # 30
print(next(it))    # raises StopIteration
A for loop calls __iter__ once to get an iterator, then calls __next__ repeatedly until StopIteration ends the loop.

for הוא קיצור עבור ”קריאה ל-__iter__ פעם אחת, ואז לולאה על __next__ עד StopIteration.“

מה ש-for x in items: עושה למעשה:

_it = iter(items)
while True:
    try:
        x = next(_it)
    except StopIteration:
        break
    # ... loop body ...

כל רשימה, tuple, מחרוזת, dict, set, אובייקט קובץ וגנרטור כבר ממשים __iter__ ו-__next__ – ולכן כולם עובדים עם for.

2.31.2. yield ופונקציות גנרטור

פונקציה המכילה הצהרת yield היא פונקציית גנרטור. קריאה לה אינה מריצה את הגוף; היא מחזירה אובייקט גנרטור (איטרטור) שמריץ את הגוף yield אחד בכל פעם:

def count_up_to(n):
    i = 0
    while i < n:
        yield i
        i += 1

for value in count_up_to(3):
    print(value)

פלט:

0
1
2

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

Sequence diagram with two lifelines (caller and generator body). The caller calls count_up_to(3), which creates a generator without running the body. Each subsequent next() runs the body until the next yield, returns the yielded value, and pauses. The fourth next() falls off the end and raises StopIteration. The variable i is preserved across pauses.

next() מריץ את הגוף עד ה-yield הבא, מחזיר את הערך, ועוצר. המצב המקומי שורד את העצירה.

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

2.31.3. צינורות עצלים

גנרטורים מתחברים היטב. הפלט של גנרטור אחד יכול להזין אחר:

def numbers():
    i = 0
    while True:
        yield i
        i += 1

def squares(source):
    for x in source:
        yield x * x

pipeline = squares(numbers())

for v in pipeline:
    if v > 100:
        break
    print(v)

הערכים זורמים דרך הצינור אחד בכל פעם – ללא רשימת ביניים, ללא חסם עליון מובנה ב-numbers, והצרכן (for v in pipeline) מחליט מתי לעצור.

Three boxes left-to-right: numbers(), squares(source), and the for-v-in-pipeline consumer. Three cycles are drawn beneath. In each cycle, the consumer sends a pull request leftward to squares, which sends a pull leftward to numbers; numbers yields a value rightward to squares, which yields its squared value rightward to the consumer.

כל next() על הצרכן מפעיל משיכה אחת דרך השרשרת; ערכים קיימים רק כאשר משהו מבקש אותם.

2.31.3.1. yield from

לולאה שמושכת פריטים מ-iterable אחר ומניבה כל אחד היא נפוצה מספיק כך ש-Python מספק קיצור. הביטוי yield from iter מניב כל ערך שה-iterable מייצר, לפי הסדר – כאילו לגנרטור הייתה לולאת for x in iter: yield x בתוכו:

def chain(*sources):
    for source in sources:
        yield from source

for v in chain([1, 2, 3], (4, 5), "abc"):
    print(v)

פלט:

1
2
3
4
5
a
b
c

yield from שקול בדיוק ללולאת ה-for המפורשת, רק קצר יותר, והוא מעביר StopIteration מה-iterable הפנימי לגנרטור החיצוני בנקיות – שימושי בעת שרשור של כמה גנרטורים קצה-לקצה.

2.31.3.2. כאשר yield נגמר

נפילה אל מעבר לסוף של פונקציית גנרטור (או פגיעה ב-return מפורש) מעלה StopIteration אוטומטית. אין צורך להעלות אותה ידנית; לולאת ה-for המקיפה רואה אותה ומסתיימת.

השתמש בגנרטורים כאשר הקוד המייצר נכתב באופן טבעי כלולאה עם מספר נקודות yield; השתמש בהבנת רשימה (list comprehension) פשוטה כאשר אתה באמת זקוק לכל הרצף בזיכרון.