2.31. Iteratoare și generatoare

Bucla for a făcut mai multă muncă decât pare. Această pagină acoperă protocolul de iterare pe care se bazează și cuvântul-cheie yield care îți permite să-ți construiești propriile iteratoare.

2.31.1. Protocolul de iterare

Fiecare obiect care poate fi parcurs în buclă implementează două metode:

  • __iter__() – întoarce un iterator peste elementele obiectului.

  • __next__() – pe iterator, întoarce elementul următor sau ridică StopIteration când nu mai există altele.

Funcția încorporată iter() apelează __iter__; next() apelează __next__. Parcurge o listă manual:

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 este o prescurtare pentru „apelează __iter__ o dată, apoi iterează pe __next__ până la StopIteration.”

Ce face for x in items: de fapt:

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

Fiecare listă, tuplu, șir, dicționar, set, obiect fișier și generator implementează deja __iter__ și __next__ – de aceea toate funcționează cu for.

2.31.2. yield și funcțiile generatoare

O funcție care conține o instrucțiune yield este o funcție generatoare. Apelarea ei nu execută corpul; întoarce un obiect generator (un iterator) care rulează corpul câte un yield o dată:

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

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

Rezultat:

0
1
2

Fiecare apel la next() reia funcția până la următorul yield, transmite acea valoare apelantului și se întrerupe acolo. Starea locală (i în acest caz) este păstrată între reluări.

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() rulează corpul până la următorul yield, transmite valoarea înapoi și se întrerupe. Starea locală supraviețuiește întreruperii.

Generatoarele sunt cel mai simplu mod de a produce o secvență în mod leneș – nu se construiește nicio listă, elementele sunt calculate doar când consumatorul le cere, iar funcția poate produce elemente la nesfârșit dacă dorește.

2.31.3. Conducte leneșe

Generatoarele se compun bine. Rezultatul unui generator poate alimenta altul:

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)

Valorile curg prin conductă câte una – fără listă intermediară, fără limită superioară încorporată în numbers, iar consumatorul (for v in pipeline) decide când să se oprească.

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.

Fiecare next() pe consumator declanșează o extragere prin lanț; valorile există doar când ceva le cere.

2.31.3.1. yield from

O buclă care extrage elemente dintr-un alt iterabil și produce fiecare element este suficient de frecventă încât Python oferă o scurtătură. Expresia yield from iter produce fiecare valoare pe care iterabilul o generează, în ordine – ca și cum generatorul ar avea o buclă for x in iter: yield x inclusă:

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

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

Rezultat:

1
2
3
4
5
a
b
c

yield from este exact echivalent cu bucla for explicită, doar mai scurt, și propagă curat StopIteration de la iterabilul interior către generatorul exterior – util la înlănțuirea mai multor generatoare cap la cap.

2.31.3.2. Când yield se epuizează

Ajungerea la finalul unei funcții generatoare (sau întâlnirea unui return explicit) ridică automat StopIteration. Nu este nevoie să o ridici manual; bucla for din jur o detectează și se încheie.

Folosește generatoare când codul producător este scris în mod natural ca o buclă cu câteva puncte yield; folosește o simplă comprehensiune de listă când chiar ai nevoie de întreaga secvență în memorie.