W3docs

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 StopIteration

Meccanismi 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 solleva StopIteration automaticamente.
  • Un ciclo for chiama next() per te e si ferma correttamente su StopIteration.

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

returnyield
Tipo di funzioneNormaleGenerator
Esecuzione dopo la chiamataVa fino al completamentoSi sospende su yield
Stato tra le chiamateScartatoPreservato
Valori multipliUno (o una tupla)Uno per yield, sequenzialmente
Memoria per grandi datiContiene tutti i valoriContiene 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 exhausted

Nota 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)   # 338350

Non 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))   # 12

Per 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))    # 35

Regole per .send():

  1. Devi chiamare next(gen) (o gen.send(None)) una volta per far avanzare il generator fino al primo yield prima di poter inviare un valore diverso da None.
  2. send(None) è equivalente a next().
  3. Il valore inviato diventa il risultato dell'espressione yield sul lato sinistro.

Stato del Generator ed Esaurimento

Un oggetto generator ha un ciclo di vita con quattro stati:

StatoDescrizione
CreatoFunzione generator chiamata, corpo non ancora avviato
In esecuzioneAttualmente in esecuzione (all'interno di una chiamata next() o send())
SospesoIn pausa su un yield; riprenderà al prossimo next()
ChiusoCorpo 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 exhausted

Se 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)  # done

Pattern 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

CaratteristicaClasse iteratoreFunzione generatorEspressione generator
SintassiClasse con __iter__/__next__def + yield(expr for x in ...)
VerbositàAltaBassaMolto bassa
Gestione dello statoManualeAutomaticaAutomatica
Logica multi-istruzioneNo (espressione singola)
Sequenze infinite
Leggibilità per logica complessaNo

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.

Pratica

Pratica
Which of the following statements about Python generators are correct?
Which of the following statements about Python generators are correct?
Was this page helpful?