Generator Python e yield
Scopri i generator Python e la parola chiave yield con esempi chiari su funzioni generator, espressioni, send() e casi d'uso reali.
Un generator è un tipo speciale di iteratore che produce valori uno alla volta, su richiesta, invece di calcolarli tutti in anticipo. I generator sono definiti usando la normale sintassi delle funzioni con yield al posto di return. Sono la soluzione idiomatica di Python per sequenze grandi o infinite dove costruire una lista completa sprecherebbe memoria o tempo.
Questo capitolo tratta la parola chiave yield, le funzioni generator rispetto alle liste, le espressioni generator, l'invio di valori in un generator, il concatenamento di generator e i pattern nel mondo reale.
Cos'è un Generator?
Quando Python chiama una funzione normale esegue il corpo fino al completamento e restituisce un valore. Quando Python chiama una funzione generator, non esegue il corpo affatto — restituisce un oggetto generator. Ogni volta che chiami next() su quell'oggetto, l'esecuzione riprende da dove si era interrotta (l'istruzione yield), va avanti fino al prossimo yield e si sospende di nuovo.
def count_up(start, stop):
while start <= stop:
yield start # pause here, emit the value
start += 1
gen = count_up(1, 3)
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
# next(gen) would now raise StopIterationMeccanismi chiave:
- Il corpo della funzione non viene eseguito fino alla prima chiamata a
next(). - Le variabili locali e il puntatore di istruzione vengono preservati tra le chiamate.
- Quando il corpo della funzione termina (o incontra un semplice
return), Python sollevaStopIterationautomaticamente. - Un ciclo
forchiamanext()per te e si ferma correttamente suStopIteration.
La Parola Chiave yield
yield è l'unica sintassi che distingue una funzione generator da una normale. Puoi usare yield ovunque possa apparire un return, inclusi cicli, condizionali e blocchi try/except.
yield vs return
return | yield | |
|---|---|---|
| Tipo di funzione | Normale | Generator |
| Esecuzione dopo la chiamata | Va fino al completamento | Si sospende su yield |
| Stato tra le chiamate | Scartato | Preservato |
| Valori multipli | Uno (o una tupla) | Uno per yield, sequenzialmente |
| Memoria per grandi dati | Contiene tutti i valori | Contiene un valore alla volta |
yield Sospende, Non Termina
def three_things():
print("about to yield first")
yield "first"
print("about to yield second")
yield "second"
print("about to yield third")
yield "third"
print("generator exhausted")
for item in three_things():
print("got:", item)Output:
about to yield first
got: first
about to yield second
got: second
about to yield third
got: third
generator exhaustedNota le istruzioni print tra i yield — il codice normale viene eseguito tra ogni sospensione.
Funzioni Generator vs Liste
Considera la generazione dei primi n numeri al quadrato. Usando una lista:
def squares_list(n):
result = []
for i in range(1, n + 1):
result.append(i * i)
return result
print(squares_list(5)) # [1, 4, 9, 16, 25]Usando un generator:
def squares_gen(n):
for i in range(1, n + 1):
yield i * i
gen = squares_gen(5)
print(list(gen)) # [1, 4, 9, 16, 25]Entrambi producono gli stessi valori, ma la versione con generator:
- Usa memoria O(1) indipendentemente da
n(la versione con lista usa O(n)) - Inizia a produrre valori immediatamente, senza aspettare di costruire l'intera collezione
- Può rappresentare sequenze infinite (una lista non può)
Quando Scegliere un Generator
Usa un generator quando:
- Devi iterare sui valori una sola volta.
- La sequenza è abbastanza grande da rendere rilevante tenerla tutta in memoria.
- Stai costruendo una pipeline di dati (un generator alimenta un altro).
- La sequenza è potenzialmente infinita (ad es., leggere righe di log da un file live).
Usa una lista quando:
- Hai bisogno di accesso casuale per indice.
- Devi iterare la stessa sequenza più volte.
- Hai bisogno di
len(), slicing o ordinamento in-place.
Espressioni Generator
Un'espressione generator è ai generator ciò che una list comprehension è alle liste. La sintassi è identica tranne per il fatto che si usano le parentesi tonde invece delle parentesi quadre:
# List comprehension — builds the full list immediately
squares_list = [x * x for x in range(1, 6)]
# Generator expression — lazy, produces one value at a time
squares_gen = (x * x for x in range(1, 6))
print(type(squares_list)) # <class 'list'>
print(type(squares_gen)) # <class 'generator'>
print(list(squares_gen)) # [1, 4, 9, 16, 25]Le espressioni generator sono più utili quando vengono passate direttamente a una funzione che consuma un iterabile:
total = sum(x * x for x in range(1, 101)) # sum of squares 1..100
print(total) # 338350Non sono necessarie parentesi aggiuntive quando l'espressione generator è l'unico argomento di una chiamata di funzione.
Filtraggio con Espressioni Generator
evens = (x for x in range(20) if x % 2 == 0)
print(list(evens)) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]Generator Infiniti
Poiché un generator produce valori in modo lazy, può rappresentare una sequenza senza fine. L'esempio classico è un contatore infinito:
def counter(start=0):
n = start
while True:
yield n
n += 1
gen = counter(10)
print(next(gen)) # 10
print(next(gen)) # 11
print(next(gen)) # 12Per consumare solo una parte di un generator infinito, usa itertools.islice o esci da un ciclo con break:
import itertools
gen = counter(1)
first_five = list(itertools.islice(gen, 5))
print(first_five) # [1, 2, 3, 4, 5]Un generator infinito pratico — la sequenza di Fibonacci:
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
print([next(fib) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]yield from — Delegare a un Sub-Generator
yield from consente a un generator di delegare a un altro iterabile, inoltrandone ciascun valore in modo trasparente:
def first_part():
yield 1
yield 2
def second_part():
yield 3
yield 4
def combined():
yield from first_part()
yield from second_part()
print(list(combined())) # [1, 2, 3, 4]yield from funziona anche con qualsiasi iterabile, non solo con i generator:
def flatten(nested):
for sublist in nested:
yield from sublist
data = [[1, 2], [3, 4], [5, 6]]
print(list(flatten(data))) # [1, 2, 3, 4, 5, 6]yield from è più pulito di un ciclo for annidato sull'iterabile interno e inoltra correttamente le chiamate send() e throw() al generator delegato (importante per i pattern di coroutine).
Invio di Valori in un Generator
I generator sono canali bidirezionali. Il metodo .send(value) riprende il generator e passa un valore indietro come risultato dell'espressione yield:
def accumulator():
total = 0
while True:
value = yield total # yield sends total out; receives value in
if value is None:
break
total += value
gen = accumulator()
next(gen) # prime the generator (advance to first yield)
print(gen.send(10)) # 10
print(gen.send(20)) # 30
print(gen.send(5)) # 35Regole per .send():
- Devi chiamare
next(gen)(ogen.send(None)) una volta per far avanzare il generator fino al primoyieldprima di poter inviare un valore diverso daNone. send(None)è equivalente anext().- Il valore inviato diventa il risultato dell'espressione
yieldsul lato sinistro.
Stato del Generator ed Esaurimento
Un oggetto generator ha un ciclo di vita con quattro stati:
| Stato | Descrizione |
|---|---|
| Creato | Funzione generator chiamata, corpo non ancora avviato |
| In esecuzione | Attualmente in esecuzione (all'interno di una chiamata next() o send()) |
| Sospeso | In pausa su un yield; riprenderà al prossimo next() |
| Chiuso | Corpo terminato o .close() chiamato; solleva StopIteration |
Una volta esaurito, iterare nuovamente un generator non produce nulla:
gen = (x for x in range(3))
print(list(gen)) # [0, 1, 2]
print(list(gen)) # [] — already exhaustedSe devi iterare l'output di un generator più di una volta, convertilo prima in una lista oppure ricrea il generator.
return All'Interno di un Generator
Un'istruzione return all'interno di un generator termina l'iterazione in modo pulito. Il valore passato a return diventa l'attributo value dell'eccezione StopIteration (raramente usato direttamente, ma importante per la delega con yield from):
def limited():
yield 1
yield 2
return "done" # StopIteration.value = "done"
gen = limited()
print(next(gen)) # 1
print(next(gen)) # 2
try:
next(gen)
except StopIteration as e:
print(e.value) # donePattern nel Mondo Reale
Lettura di un File di Grandi Dimensioni Riga per Riga
def read_lines(filepath):
with open(filepath) as f:
for line in f:
yield line.rstrip("\n")
# Memory usage stays constant regardless of file size
for line in read_lines("/etc/hosts"):
if line.startswith("#"):
continue
print(line)Costruzione di una Pipeline di Dati
I generator si compongono naturalmente in pipeline dove ogni fase trasforma il flusso:
def integers(n):
for i in range(1, n + 1):
yield i
def only_even(nums):
for n in nums:
if n % 2 == 0:
yield n
def squared(nums):
for n in nums:
yield n * n
# Compose: even squares from 1..20
pipeline = squared(only_even(integers(20)))
print(list(pipeline))
# [4, 16, 36, 64, 100, 144, 196, 256, 324, 400]Ogni fase è lazy — i valori fluiscono attraverso la pipeline uno alla volta senza costruire liste intermedie.
Suddivisione di un Iterabile in Blocchi
def chunks(iterable, size):
chunk = []
for item in iterable:
chunk.append(item)
if len(chunk) == size:
yield chunk
chunk = []
if chunk:
yield chunk
data = list(range(10))
for batch in chunks(data, 3):
print(batch)
# [0, 1, 2]
# [3, 4, 5]
# [6, 7, 8]
# [9]Generator vs Iteratori vs Comprehension
| Caratteristica | Classe iteratore | Funzione generator | Espressione generator |
|---|---|---|---|
| Sintassi | Classe con __iter__/__next__ | def + yield | (expr for x in ...) |
| Verbosità | Alta | Bassa | Molto bassa |
| Gestione dello stato | Manuale | Automatica | Automatica |
| Logica multi-istruzione | Sì | Sì | No (espressione singola) |
| Sequenze infinite | Sì | Sì | Sì |
| Leggibilità per logica complessa | Sì | Sì | No |
Per qualcosa di più di una semplice trasformazione o filtro, una funzione generator è più leggibile di un'espressione generator. Per iterazioni stateful complesse, una funzione generator è quasi sempre preferibile alla scrittura di una classe iteratore completa — vedi Python Iterators per l'approccio basato su classi.
Le espressioni generator si abbinano naturalmente alle list comprehension e alle comprehension di dizionari e set. Anche i decorator possono avvolgere funzioni generator per aggiungere comportamenti di caching o tracing.