# Generatori in Python: yield, iteratori e list comprehension

URL: https://skillaria.top/it/guides/python/generatori-python

Un generatore in Python è un oggetto che restituisce i valori uno alla volta, su richiesta, senza tenerli tutti in memoria; si crea con una funzione che usa la parola yield oppure con un'espressione generatrice tra parentesi tonde. Le list, dict e set comprehension sono la scrittura breve per costruire una collezione completa in una riga: [x * 2 for x in xs], {k: v for k, v in pairs}, {x for x in xs}.

Un generatore in Python è un oggetto che restituisce i valori uno alla volta, solo quando vengono richiesti, senza tenere in memoria l'intera sequenza. Si crea in due modi: con una funzione che usa la parola chiave `yield` oppure con un'espressione generatrice tra parentesi tonde. Accanto ai generatori ci sono le list, dict e set comprehension, cioè la scrittura sintetica per costruire una collezione in una sola riga. Vediamo come funziona yield, in che cosa un generatore si differenzia da un iteratore e come evitare che una comprehension diventi illeggibile.

## Iteratori: su che cosa si basa il ciclo for

Per capire i generatori bisogna prima conoscere gli iteratori. Un oggetto iterabile (iterable) è qualcosa su cui si può fare un ciclo: lista, stringa, dizionario, file. Un iteratore è un oggetto che ricorda la posizione corrente e, a ogni chiamata di `next()`, restituisce l'elemento successivo; quando gli elementi finiscono, solleva `StopIteration`.

```
nums = [10, 20, 30]
it = iter(nums)      # ottiene un iteratore dalla lista
print(next(it))      # 10
print(next(it))      # 20
print(next(it))      # 30
next(it)             # StopIteration
```

Il ciclo `for` fa esattamente questo: chiama `iter()`, poi `next()` fino a `StopIteration`. Un iteratore personalizzato si può scrivere come classe con i metodi `__iter__` e `__next__`:

```
class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current  200]                    # [250, 400]
labels = ["molto" if a > 200 else "poco" for a in amounts]
# ['poco', 'molto', 'molto']

# filtro e scelta insieme
parity = ["pari" if n % 2 == 0 else "dispari" for n in range(6) if n > 2]
# ['dispari', 'pari', 'dispari']
```

La scrittura `[a for a in amounts if a > 200 else 0]` è un errore di sintassi: `else` è ammesso solo nella condizione iniziale.

### Comprehension annidate e liste bidimensionali

Il doppio `for` si scrive nello stesso ordine dei cicli annidati, dall'alto verso il basso. Questo aiuta a non confondere l'ordine quando si appiattisce una lista di liste:

```
groups = [[1, 2], [3], [4, 5]]
flat = [x for g in groups for x in g]
# equivalente a:
# for g in groups:
#     for x in g:
#         flat.append(x)
print(flat)  # [1, 2, 3, 4, 5]
```

Una matrice bidimensionale si crea con una comprehension dentro un'altra. L'errore classico è moltiplicare la lista: le righe finirebbero per essere lo stesso oggetto.

```
bad = [[0] * 3] * 2
bad[0][0] = 1
print(bad)   # [[1, 0, 0], [1, 0, 0]] — sono cambiate entrambe le righe

grid = [[0] * 3 for _ in range(2)]
grid[0][0] = 1
print(grid)  # [[1, 0, 0], [0, 0, 0]]

# trasposizione
m = [[1, 2, 3], [4, 5, 6]]
print([[row[i] for row in m] for i in range(3)])  # [[1, 4], [2, 5], [3, 6]]
```

## Dict e set comprehension

Lo stesso schema funziona per le altre collezioni. Le parentesi graffe con i due punti costruiscono un dizionario (dict comprehension), quelle senza due punti costruiscono un insieme (set comprehension).

```
spent = [("cibo", 500), ("taxi", 300), ("cibo", 200)]

categories = {cat for cat, _ in spent}
print(categories)  # {'cibo', 'taxi'} — l'ordine nel set non è garantito

last = {cat: amount for cat, amount in spent}
print(last)        # {'cibo': 200, 'taxi': 300}

squares = {n: n ** 2 for n in range(4)}  # {0: 0, 1: 1, 2: 4, 3: 9}
swapped = {v: k for k, v in squares.items()}
```

Notate `last`: in caso di chiave ripetuta vince l'ultimo valore, e il 500 si perde. Se bisogna sommare, la comprehension non va bene: servono un ciclo oppure `collections.Counter` / `defaultdict`.

### E la tuple comprehension?

In Python non esiste una «tuple comprehension» separata. Le parentesi tonde danno un'espressione generatrice, mentre una tupla si ottiene passandola a `tuple()`:

```
t = tuple(n * 2 for n in range(3))  # (0, 2, 4)
```

## Espressione generatrice: produrre invece di memorizzare

Se si sostituiscono le parentesi quadre con quelle tonde, si ottiene un'espressione generatrice. Non calcola nulla in anticipo e restituisce i valori uno per uno. Per un milione di elementi la differenza di memoria è enorme:

```
import sys

as_list = [n for n in range(1_000_000)]
as_gen = (n for n in range(1_000_000))
print(sys.getsizeof(as_list))  # circa 8 MB
print(sys.getsizeof(as_gen))   # un paio di centinaia di byte
```

Le espressioni generatrici vivono spesso dentro le funzioni di aggregazione. Se il generatore è l'unico argomento, le parentesi aggiuntive non servono. `any()` e `all()` inoltre si fermano alla prima risposta e non scorrono il resto:

```
spent = [("cibo", 500), ("taxi", 300), ("cibo", 200)]

total = sum(a for _, a in spent)            # 1000
has_big = any(a > 400 for _, a in spent)    # True, oltre 500 non guarda
biggest = max(spent, key=lambda p: p[1])    # ('cibo', 500)
```

La regola di scelta è semplice: se il risultato serve più volte, o servono indicizzazione e `len()`, prendete una lista. Se bisogna percorrere una volta sola e calcolare qualcosa, prendete un generatore.

## Ambito delle variabili: la variabile non esce

Una domanda che mette in difficoltà anche chi programma da tempo:

```
x = 10
squares = [x for x in range(3)]
print(x)  # 10
```

La variabile del ciclo dentro una comprehension vive nel proprio ambito e non sovrascrive quella esterna (così avviene in Python 3; in Python 2 era diverso). In un normale ciclo `for`, dopo di esso `x` varrebbe 2. Dalla versione 3.12 (PEP 709) le comprehension vengono integrate nella funzione che le contiene e sono più veloci, ma la regola di visibilità è rimasta la stessa.

## Unpacking * e ** nelle comprehension: novità di Python 3.15 (PEP 798)

L'asterisco per lo unpacking si poteva scrivere da tempo nelle chiamate di funzione e nei letterali di lista, ma dentro una comprehension era un `SyntaxError`. In Python 3.15 il divieto è stato rimosso: `*elemento` significa «inserisci qui tutti i suoi elementi». Funziona nelle liste, negli insiemi e nei generatori, anche asincroni.

```
# Python 3.15+
groups = [[1, 2], [3], [4, 5]]
flat = [*g for g in groups]        # [1, 2, 3, 4, 5]
uniq = {*g for g in groups}        # insieme
lazy = (*g for g in groups)        # generatore pigro
```

Il doppio asterisco fa lo stesso con i dizionari. È comodo per le configurazioni a strati «predefinite → file → flag»: ogni strato successivo sovrascrive le chiavi del precedente.

```
# Python 3.15+
layers = [{"debug": False, "port": 80}, {"port": 8080}, {"debug": True}]
config = {**layer for layer in layers}
# {'debug': True, 'port': 8080}
```

Due avvertenze dal corso. L'asterisco non si può combinare senza parentesi con un'espressione condizionale: `[*w if w else 0 for w in weeks]` è un errore di sintassi, la condizione va racchiusa tra parentesi. Inoltre questo codice non gira su Python 3.14 e versioni precedenti: se la libreria supporta anche le versioni vecchie, lasciate il doppio `for` oppure `itertools.chain.from_iterable`.

## Generatori asincroni

Se in una funzione `async def` c'è un `yield`, si ottiene un generatore asincrono. Lo si percorre con `async for`, e al suo interno si può attendere la rete o il disco con `await`. Esistono anche le comprehension asincrone.

```
import asyncio

async def ticks(n):
    for i in range(n):
        await asyncio.sleep(0.1)
        yield i

async def main():
    result = [i async for i in ticks(3)]
    print(result)  # [0, 1, 2]

asyncio.run(main())
```

## Errori tipici

- **Comprehension su tre piani.** Due `for`, due `if` e un ternario in un'unica riga nessuno lo legge dopo una settimana. Se non entra in una riga chiara, scrivete un ciclo normale: la PEP 8 mette la leggibilità davanti alla brevità.
- **Secondo passaggio su un generatore.** Un secondo `sum(gen)` restituisce 0, perché il generatore è già esaurito.
- **Comprehension per un effetto collaterale.** `[print(x) for x in xs]` costruisce una lista inutile di `None`. Per le azioni usate un ciclo `for`.
- **Aspettarsi una lista da un'espressione generatrice.** `(x for x in xs)[0]` dà `TypeError`: i generatori non hanno indici.

## Esercizi sui generatori

1. Scrivete un generatore `fib()` che produca all'infinito i numeri di Fibonacci e prendete i primi 10 con `islice`.
2. In una sola riga ottenete un dizionario «parola → lunghezza» per le parole di più di tre lettere.
3. Calcolate la somma dei quadrati dei numeri pari fino a un milione senza creare una lista.
4. Scrivete un generatore `chunks(seq, n)` che produca pezzi di lista di `n` elementi.

## Conclusioni

L'iteratore restituisce gli elementi uno alla volta tramite `next()`; il generatore è il modo più semplice per scriverne uno: una funzione con yield oppure un'espressione tra parentesi tonde. Le list, dict e set comprehension costruiscono una collezione completa in una riga: il filtro `if` va alla fine, la scelta `if else` all'inizio. Se bisogna risparmiare memoria o elaborare un flusso di dati una volta sola, prendete un generatore; se il risultato serve più volte, una lista. Comprehension, generatori e la nuova unpacking di Python 3.15 sono trattati in pratica nel corso, sull'esempio del tracker delle spese.

## FAQ

**Si può usare return in un generatore?**

Sì. return senza valore termina semplicemente il generatore; return con un valore lo inserisce nell'attributo value dell'eccezione StopIteration, e quel valore viene ricevuto dall'espressione yield from.

**Come trasformare un generatore in lista?**

Passatelo a list(): list(gen). Dopo questa operazione il generatore è esaurito, e tutti i valori finiscono in memoria.

**Una list comprehension è più veloce di un ciclo con append?**

Di solito sì, di poco, perché non chiama il metodo append a ogni iterazione. Però la scelta va fatta in base alla leggibilità: la differenza raramente è decisiva.

**Come verificare che un oggetto sia un generatore?**

Usate inspect.isgenerator(obj) per un oggetto o inspect.isgeneratorfunction(f) per una funzione; per verificare che un oggetto sia un iteratore si usa isinstance(obj, collections.abc.Iterator).

**In che cosa l'espressione generatrice si differenzia dalla list comprehension?**

Le parentesi quadre costruiscono subito l'intera lista in memoria; le tonde creano un generatore che calcola gli elementi su richiesta e può essere percorso una sola volta.


---
Source: Skillaria.Top (https://skillaria.top)
