Corso Python / Iteratori, generatori e yield

Un iteratore fornisce un elemento alla volta; un generatore permette di scrivere questa sequenza con yield, calcolando i valori quando vengono richiesti. Consumare un valore fa avanzare la posizione: un iteratore esaurito non ricomincia automaticamente dall’inizio.

Distinguere iterabile, iteratore e generatore

Una lista è iterabile: puoi ottenere con iter(lista) un iteratore che ne attraversa gli elementi. next(iteratore) richiede il valore successivo e solleva StopIteration quando non ne rimangono. Il ciclo for gestisce questo segnale per terminare l’iterazione.

Una funzione contenente yield restituisce un generatore quando viene chiamata. Il corpo non viene eseguito subito fino in fondo: ogni richiesta del valore successivo lo fa avanzare, e yield sospende l’esecuzione mantenendo lo stato locale necessario a riprendere. La guida ufficiale ai generatori descrive questo comportamento.

Per esempio, un totale progressivo deve ricordare la somma corrente tra un elemento e il successivo. Non ha però bisogno di costruire tutti i totali in una lista prima di consegnare il primo risultato.

Consumare totali progressivi un passo alla volta

Salva il codice completo come generators.py, in UTF-8, in una cartella di esercizio. Esegui python generators.py nel terminale con l’interprete del progetto selezionato. Fuori dall’ambiente virtuale su macOS o Ubuntu usa python3 quando è il comando scelto. Non servono librerie esterne.

def running_totals(amounts):
    total = 0
    for amount in amounts:
        total += amount
        yield total

values = running_totals([3500, 8000, 2500])
print(next(values))
print(list(values))
print(list(values))
print(list(running_totals([3500, 8000, 2500])))

Il risultato previsto è:

3500
[11500, 14000]
[]
[3500, 11500, 14000]

La prima next consuma 3500. Il primo list raccoglie soltanto i totali rimasti, 11500 e 14000; la seconda conversione sullo stesso generatore è vuota. L’ultima riga chiama nuovamente running_totals e crea quindi un generatore diverso, che produce la sequenza completa.

Se devi usare due volte un risultato piccolo, puoi raccoglierlo una sola volta in una lista e riutilizzare quella lista. Creare un generatore nuovo funziona invece solo se anche la sorgente può fornire di nuovo i dati: se passi lo stesso iteratore di ingresso già esaurito, il generatore nuovo non ricostruisce ciò che è stato consumato.

Valutare memoria, posizione e calcolo su richiesta

Il generatore evita di conservare in anticipo l’intera sequenza di risultati, ma non elimina la memoria occupata da un ingresso che hai già costruito come grande lista. Anche chiamare list sul generatore materializza tutti i risultati residui. L’elaborazione su richiesta cambia i tempi di valutazione e il modo di conservare i dati; non garantisce da sola maggiore velocità.

Salva questo secondo programma completo come iterator.py, mantenendo il primo file per il confronto, ed esegui python iterator.py:

values = [1, 2, 3]
iterator = iter(values)
print(next(iterator))
print(list(iterator))
print(list(iterator))
print(list(values))
expression = (n * 2 for n in values)
print(list(expression))

Il risultato previsto è:

1
[2, 3]
[]
[1, 2, 3]
[2, 4, 6]

L’iteratore avanza, ma la lista values conserva gli elementi originali e può essere attraversata di nuovo. L’espressione (n * 2 for n in values) crea un generatore che calcola ogni doppio quando viene consumato. Dopo il primo list(expression), anche quel generatore è esaurito.

Il generatore non implica una copia isolata dell’ingresso: se conservi un riferimento a una lista e la modifichi prima di consumarlo, i risultati possono riflettere quelle modifiche. Stabilisci se l’elaborazione deve osservare i dati correnti o una fotografia dei dati e scegli di conseguenza quando copiarli o raccoglierli.

Gestire errori rinviati e durata delle risorse

Un errore nel corpo del generatore può presentarsi durante next o list, non durante la creazione. Per esempio, se un importo non può essere sommato, il generatore può aver già prodotto alcuni totali prima del TypeError. Metti la gestione dell’errore intorno alla fase che consuma i dati quando è lì che il problema può emergere.

Un generatore che apre un file può tenerlo aperto mentre è sospeso. Interrompere un ciclo con break non significa automaticamente chiudere ogni generatore ancora referenziato. Quando possibile, lascia che il chiamante gestisca il file con with e consumi i dati dentro quel blocco: la guida ai file di testo spiega il confine di chiusura.

Se il generatore possiede la risorsa, devi progettarne anche la chiusura esplicita; il metodo close può far eseguire la pulizia prevista da un finally in un generatore sospeso. La documentazione dei metodi dei generatori descrive le condizioni. Non basare una procedura importante sul momento in cui un particolare interprete libererà un oggetto non più usato.

  1. Nel primo programma aggiungi una seconda print(next(values)) subito dopo la prima: deve apparire 11500 e il successivo list deve contenere solo [14000].
  2. In una prova separata usa un ingresso vuoto per la prima chiamata: la prima next senza valore predefinito deve sollevare StopIteration. Raccogliere invece tutto il generatore vuoto con list produce [].
  3. Ripristina i dati originali e crea un nuovo generatore per ogni lettura completa, oppure conserva la lista dei risultati se vuoi riutilizzarli. Ricontrolla infine le quattro righe dell’output originale.