Menu

Generatori in Python: yield, iterazione lazy e generator expression

Come i generatori producono valori in modo lazy in Python: la parola chiave yield, le generator expression e quando battono una semplice lista.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Una funzione che si mette in pausa

Un generatore sembra una funzione normale, ma invece di calcolare un risultato completo e restituirlo, cede un valore alla volta con yield, mettendosi in pausa tra un valore e l'altro finché chi lo usa non chiede il successivo.

Il più semplice possibile:

Nota yield al posto di return. La prima volta che il for chiede un valore, Python esegue il corpo della funzione finché non incontra yield 1. La funzione si ferma proprio lì, passa 1 al ciclo e ricorda esattamente dove si è fermata, variabili comprese. L'iterazione successiva riparte da dove si era interrotta: current += 1, di nuovo al while, yield 2. E così via finché la condizione del ciclo non diventa falsa: a quel punto il generatore semplicemente si ferma.

Tutto il trucco sta in questo mettersi in pausa e ripartire.

Perché non costruire semplicemente una lista?

Perché la versione con la lista alloca tutti i valori in anticipo:

Va bene per 5 elementi. Ora immagina di volere 50 milioni di interi, e che ti interessi solo il primo che soddisfa una certa condizione. La versione con la lista alloca 50 milioni di interi e poi ne butti via quasi tutti. La versione con il generatore ne crea esattamente quanti ne consuma chi chiama. Quando il ciclo for trova quello che cerca ed esce con break, il generatore semplicemente si ferma.

È questo lo schema da interiorizzare: i generatori ti permettono di scrivere codice di iterazione senza decidere in anticipo quanta parte del risultato ti servirà.

Generator expression

Se hai scritto una list comprehension, conosci già la sintassi: sostituisci le parentesi quadre con quelle tonde:

squares_gen non calcola ancora niente. È solo una ricetta. Scorrerlo esegue la ricetta un passo alla volta.

Le generator expression sono perfette come argomenti per le funzioni che consumano un iterabile:

Nessuna lista intermedia. sum, max e any leggono i valori uno alla volta, che è esattamente quello che vogliono.

Leggere un file grande, riga per riga

È il caso reale per eccellenza dei generatori: elaborare un file troppo grande per caricarlo in memoria:

def parse_log_lines(path):
    with open(path) as f:
        for line in f:
            if line.startswith("ERROR"):
                yield line.rstrip()

for error in parse_log_lines("app.log"):
    print(error)

Il file viene letto in modo lazy. Ogni richiesta al generatore prende una riga dal disco, la filtra e la cede. L'uso della memoria resta costante a prescindere dalla dimensione del file.

Una volta e basta

Un generatore si può attraversare una volta sola. Dopo che l'hai scorso fino in fondo, è esaurito:

Il secondo ciclo non stampa niente. Al generatore non è rimasto nulla.

Se devi scorrere più di una volta, chiama di nuovo la funzione generatore per averne uno nuovo, oppure raccogli la sequenza con list(...) e scorri la lista quante volte vuoi. Scegli in base al costo: ricostruire va bene se il lavoro costa poco; una lista va bene se la sequenza è piccola.

next() e l'iterazione manuale

Non sei obbligato a usare un ciclo for. next() estrae un valore alla volta:

StopIteration è il modo in cui un generatore segnala "ho finito". I cicli for lo catturano in silenzio. Nel codice manuale puoi passare un valore predefinito a next(gen, default) per evitare l'eccezione.

Generatori infiniti

Visto che i valori vengono prodotti su richiesta, un generatore può rappresentare una sequenza senza fine, purché chi la consuma a un certo punto smetta di chiedere:

Un while True con dentro uno yield non blocca il programma: significa solo "se qualcuno continua a chiedere, continua a produrre". È chi consuma a decidere quando fermarsi.

Questo schema compare nei flussi di dati in streaming, negli event loop e ovunque tu prenda valori da una sorgente che non ha una lunghezza definita.

yield from: delegare a un altro iterabile

Se il tuo generatore vuole cedere tutti i valori di un altro iterabile, yield from lo fa in una riga:

Senza yield from scriveresti un ciclo for annidato con dentro yield x. Inoltra anche correttamente le chiamate a send() e throw(), se mai le userai, ma per il codice di tutti i giorni pensalo come "cedi ogni valore di questa cosa".

Quando usare un generatore

Tre segnali che un generatore è lo strumento giusto:

  1. La sequenza è grande, forse infinita, o costosa da produrre per intero.
  2. Chi la consuma potrebbe fermarsi prima della fine (per esempio con un break alla prima corrispondenza).
  3. Vuoi concatenare trasformazioni (filtrare, mappare, prendere i primi n) senza costruire liste intermedie.

E quando non usarlo:

  • Ti serve l'accesso casuale (seq[42]). I generatori vanno solo avanti.
  • Devi scorrere la stessa sequenza più volte. Usa una lista.
  • La sequenza è piccola e ce l'hai già. Una list comprehension è più semplice.

Generatori, list comprehension e liste semplici sono ognuno la risposta giusta per lavori diversi. L'abilità sta nello sceglierne uno senza pensarci troppo, e il modo più rapido per sviluppare questo istinto è chiederti, per ogni iterazione che scrivi, se è meglio "produrre tutto subito" o "produrre un elemento alla volta".

Prossimo argomento: i context manager nel dettaglio

Ora hai visto la maggior parte degli idiomi che Python usa per l'iterazione. Il prossimo argomento sono i context manager, cioè l'istruzione with, che si abbinano bene ai generatori per leggere in streaming dati da file e connessioni di rete.

Domande frequenti

Cos'è un generatore in Python?

Un generatore è una funzione che produce valori uno alla volta, mettendosi in pausa tra l'uno e l'altro. Lo scrivi con def come una funzione normale, ma usi yield invece di return. Chiamarlo restituisce un oggetto generatore; ogni iterazione di un for o ogni chiamata a next() esegue la funzione fino al yield successivo.

Qual è la differenza tra una lista e un generatore?

Una lista tiene in memoria tutti gli elementi insieme. Un generatore calcola gli elementi su richiesta e li dimentica dopo che sono stati consumati. Per sequenze grandi o infinite, i generatori usano una quantità di memoria minima e fissa; per risultati piccoli che ti servono più volte, è meglio una lista.

Posso scorrere un generatore due volte?

No. Un generatore si esaurisce dopo il primo passaggio completo: un secondo ciclo for su di esso non produce niente. Se devi scorrerlo più di una volta, chiama di nuovo la funzione generatore per ottenerne uno nuovo, oppure raccogli i risultati in una lista.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA