2.31. Iteratori e generatori

Il ciclo for ha sempre fatto più lavoro di quanto sembri. Questa pagina illustra il protocollo iteratore su cui si basa, e la parola chiave yield che ti permette di costruire i tuoi iteratori.

2.31.1. Il protocollo iteratore

Ogni oggetto su cui si può iterare implementa due metodi:

  • __iter__() – restituisce un iteratore sugli elementi dell’oggetto.

  • __next__() – sull’iteratore, restituisce l’elemento successivo o solleva StopIteration quando non ce ne sono più.

La funzione integrata iter() chiama __iter__; next() chiama __next__. Scorri una lista manualmente:

it = iter([10, 20, 30])
print(next(it))    # 10
print(next(it))    # 20
print(next(it))    # 30
print(next(it))    # raises StopIteration
Un ciclo for chiama __iter__ una volta per ottenere un iteratore, poi chiama __next__ ripetutamente finché StopIteration non termina il ciclo.

for è zucchero sintattico per «chiama __iter__ una volta, poi cicla su __next__ finché non si verifica StopIteration

Cosa fa effettivamente for x in items::

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

Ogni lista, tupla, stringa, dict, set, oggetto file e generatore implementa già __iter__ e __next__ – ed è per questo che funzionano tutti con for.

2.31.2. yield e funzioni generatore

Una funzione che contiene un’istruzione yield è una funzione generatore. Chiamarla non esegue il corpo; restituisce un oggetto generatore (un iteratore) che esegue il corpo un yield alla volta:

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

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

Output:

0
1
2

Ogni chiamata a next() riprende la funzione fino al successivo yield, passa quel valore al chiamante e si sospende lì. Lo stato locale (i in questo caso) viene preservato tra una ripresa e l’altra.

Diagramma di sequenza con due linee di vita (il chiamante e il corpo del generatore). Il chiamante chiama count_up_to(3), che crea un generatore senza eseguire il corpo. Ogni successiva next() esegue il corpo fino al successivo yield, restituisce il valore prodotto e si sospende. La quarta next() arriva alla fine e solleva StopIteration. La variabile i viene preservata tra le sospensioni.

next() esegue il corpo fino al successivo yield, restituisce il valore e si sospende. Lo stato locale sopravvive alla sospensione.

I generatori sono il modo più semplice per produrre una sequenza in modo pigro – non viene costruita alcuna lista, gli elementi vengono calcolati solo quando il consumatore li richiede, e la funzione può produrre elementi all’infinito se lo desidera.

2.31.3. Pipeline pigre

I generatori si compongono bene. L’output di un generatore può alimentarne un altro:

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)

I valori scorrono attraverso la pipeline uno alla volta – nessuna lista intermedia, nessun limite superiore integrato in numbers, ed è il consumatore (for v in pipeline) a decidere quando fermarsi.

Tre riquadri da sinistra a destra: numbers(), squares(source) e il consumatore for-v-in-pipeline. Sotto sono disegnati tre cicli. In ogni ciclo, il consumatore invia una richiesta di estrazione verso sinistra a squares, che invia un'estrazione verso sinistra a numbers; numbers produce un valore verso destra a squares, che produce il suo valore al quadrato verso destra al consumatore.

Ogni next() sul consumatore innesca un’estrazione attraverso la catena; i valori esistono solo quando qualcosa li richiede.

2.31.3.1. yield from

Un ciclo che estrae elementi da un altro iterabile e produce ciascuno di essi è abbastanza comune da far sì che Python fornisca una scorciatoia. L’espressione yield from iter produce ogni valore generato dall’iterabile, in ordine – come se il generatore avesse un ciclo for x in iter: yield x inline:

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

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

Output:

1
2
3
4
5
a
b
c

yield from è esattamente equivalente al ciclo for esplicito, solo più breve, e propaga StopIteration dall’iterabile interno al generatore esterno in modo pulito – utile quando si concatenano più generatori uno dopo l’altro.

2.31.3.2. Quando yield si esaurisce

Arrivare alla fine di una funzione generatore (o incontrare un return esplicito) solleva automaticamente StopIteration. Non è necessario sollevarla manualmente; il ciclo for circostante la rileva e termina.

Usa i generatori quando il codice produttore è scritto naturalmente come un ciclo con alcuni punti yield; usa una semplice list comprehension quando hai realmente bisogno dell’intera sequenza in memoria.