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 sollevaStopIterationquando 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
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.
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.
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.