Python: generatori o liste? Memoria, velocità e quando conviene
Una list comprehension costruisce subito tutti gli elementi; una generator expression li produce uno alla volta, solo quando servono. La sintassi cambia di due caratteri ([...] contro (...)), il comportamento molto di più.
Memoria: la differenza vera
sum([x * 2 for x in range(1_000_000)]) # lista
sum(x * 2 for x in range(1_000_000)) # generatore
Misurato con tracemalloc su 1 milione di elementi (Python 3.12.15):
- lista: picco di circa 39 MB
- generatore: picco trascurabile (l'oggetto generatore pesa 192 byte, la sola lista vuota di risultati ne occupa 8 MB)
Il risultato è identico (999999000000).
Velocità: non è una scorciatoia
Sullo stesso esempio i tempi sono simili: circa 123 ms per la lista e 137 ms per il generatore (miglior tempo di 7 esecuzioni). Il generatore non è più veloce per definizione: risparmia memoria, non cicli. Se ti serve scorrere i dati più volte o conoscerne la lunghezza, la lista è la scelta giusta.
Dove il generatore vince davvero: il corto circuito
any([x == 10 for x in range(1_000_000)]) # ~64 ms
any(x == 10 for x in range(1_000_000)) # ~0,002 ms
Con la lista Python costruisce comunque tutto il milione di elementi prima di chiamare any(). Con il generatore si ferma al primo True. Vale per any, all, next, min/max con early exit manuale e per qualsiasi ricerca del "primo che soddisfa".
La trappola: un generatore si consuma una volta sola
g = (x for x in range(3))
list(g) # [0, 1, 2]
list(g) # []
La seconda lettura restituisce una lista vuota, senza errori: è uno dei bug più silenziosi. Se devi riusare i dati, usa una lista oppure ricrea il generatore.
Regola pratica
- Un solo passaggio, dati grandi o ricerca con uscita anticipata: generatore.
- Più passaggi,
len(), indicizzazione, ordinamento: lista. - Dentro
sum(),any(),all(),"".join(): puoi togliere le parentesi quadre.
In Django lo stesso principio vale per i queryset: .iterator() scorre i risultati a blocchi invece di caricarli tutti in memoria, e exists() ti evita di caricare righe solo per sapere se ce ne sono (vedi exists, count o len).
Script di verifica: verifica/generatori.py, eseguito con Python 3.12.15.