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
A for loop calls __iter__ once to get an iterator, then calls __next__ repeatedly until StopIteration ends the loop.

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.

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() 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.

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.

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.