Se scrivi Python e usi ancora i cicli for per costruire liste o elaborare dati, stai sprecando risorse. Lo vediamo nei progetti che ci arrivano: codice più lento, memoria occupata inutilmente, carichi server che lievitano senza motivo. Noi, di Meteora Web, ragioniamo in termini di efficienza e costo. Una list comprehension ben scritta non è solo più elegante: consuma meno cicli CPU e rende il codice manutenibile. Un generatore, invece di caricare tutto in RAM, può ridurre l'uso di memoria del 90% su dataset reali. Questa guida ti porta dritto al punto: capire come funzionano, quando usarli e quando no, con esempi che puoi copiare e testare subito.
List Comprehension: sintassi, potenza e limiti
La list comprehension è il modo Pythonico per creare liste in una riga. Sembra magia, ma è solo sintassi compatta. La struttura base è [espressione for elemento in iterabile if condizione]. L'ordine è lo stesso di un ciclo annidato: prima la for, poi eventuale if.
Esempio pratico: filtrare ordini
Immagina di avere una lista di ordini e volere solo quelli sopra i 100€. Con un ciclo classico:
ordini = [45, 120, 80, 200, 55]
ordini_filtrati = []
for prezzo in ordini:
if prezzo > 100:
ordini_filtrati.append(prezzo)
print(ordini_filtrati) # [120, 200]Con list comprehension:
ordini_filtrati = [prezzo for prezzo in ordini if prezzo > 100]
print(ordini_filtrati) # [120, 200]Meno righe, stessa leggibilità. Ma attenzione: non esagerare. Se la condizione è complessa o devi annidare tre for, meglio un ciclo normale. La leggibilità conta più della brevità.
Operazioni su ogni elemento
Puoi trasformare i dati al volo. Esempio: applicare IVA del 22% a una lista di prezzi netti:
netti = [10, 25, 50]
con_iva = [round(p * 1.22, 2) for p in netti]
print(con_iva) # [12.2, 30.5, 61.0]List comprehension nidificate
Funzionano come cicli annidati. Per appiattire una matrice:
matrice = [[1,2,3], [4,5], [6]]
appiattita = [num for riga in matrice for num in riga]
print(appiattita) # [1, 2, 3, 4, 5, 6]Attenzione all'ordine: prima il ciclo esterno, poi quello interno. Leggi da sinistra a destra come fossero annidati.
Generatori: quando la memoria è un problema
Una list comprehension costruisce l'intera lista in memoria. Se hai un milione di elementi, occupi diversi MB di RAM. Un generatore, invece, produce un elemento alla volta – lazy evaluation. La sintassi è identica, ma con parentesi tonde invece di quadre.
numeri = range(1_000_000)
# Generatore: non occupa memoria
gen = (x * 2 for x in numeri)
print(next(gen)) # 0
print(next(gen)) # 2Non puoi indicizzare un generatore né conoscerne la lunghezza senza esaurirlo. Ma puoi iterarci sopra con un for o convertirlo in lista (perdendo il vantaggio).
Yield: generatori fatti su misura
Con yield crei funzioni generatrici. Esempio: leggere un file di log riga per riga senza caricarlo tutto in RAM:
def leggi_log(path):
with open(path, 'r') as f:
for riga in f:
yield riga.strip()
for entry in leggi_log('server.log'):
if 'ERROR' in entry:
print(entry)Ogni chiamata a next() riprende l'esecuzione dal punto in cui si era fermata. È come un ciclo congelato. Utile per pipeline di dati o flussi infiniti.
Iteratori: il motore sotto il cofano
List comprehension, generatori, cicli for – tutto si basa sugli iteratori. Un iterabile è un oggetto che restituisce un iteratore tramite iter(). L'iteratore implementa __next__() e solleva StopIteration quando finisce.
numeri = [1, 2, 3]
it = iter(numeri)
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
# print(next(it)) # StopIterationPuoi creare iteratori personalizzati definendo __iter__ e __next__. Esempio: un contatore che parte da un valore e si ferma a un limite:
class Contatore:
def __init__(self, start, stop):
self.current = start
self.stop = stop
def __iter__(self):
return self
def __next__(self):
if self.current > self.stop:
raise StopIteration
valore = self.current
self.current += 1
return valore
for num in Contatore(1, 5):
print(num) # 1 2 3 4 5I generatori sono un modo più semplice per scrivere iteratori, ma capire il meccanismo ti permette di debuggarli e ottimizzarli.
Confronto performance: list comprehension vs generatori
Noi misuriamo sempre. Con il modulo timeit puoi testare:
import timeit
# List comprehension: 1 milione di quadrati
list_time = timeit.timeit('[x2 for x in range(1_000_000)]', number=1)
print(f"List comprehension: {list_time:.4f} sec") # ~0.06 sec
# Generatore: stesso calcolo, ma lazy
gen_time = timeit.timeit('(x2 for x in range(1_000_000))', number=1)
print(f"Generatore: {gen_time:.4f} sec") # ~0.000001 sec (non esegue!)
# Se poi iteriamo il generatore
gen = (x**2 for x in range(1_000_000))
def consuma():
for _ in gen:
pass
gen_iter_time = timeit.timeit('consuma()', globals={'consuma': consuma}, number=1)
print(f"Generatore iterato: {gen_iter_time:.4f} sec") # ~0.08 secMorale: la creazione di un generatore è istantanea, ma l'iterazione totale è simile alla list comprehension. Il vero guadagno è quando non devi consumare tutto. Ad esempio, se cerchi il primo elemento che soddisfa una condizione, un generatore si ferma subito.
Errori comuni e come evitarli
- Usare list comprehension quando serve un generatore: se devi passare i dati a una funzione che accetta un iterabile (es.
sum(),any(),max()), non serve creare una lista intermedia. Usa un generatore:sum(x**2 for x in range(10)). - Riutilizzare un generatore: dopo averlo esaurito, è vuoto. Se devi iterare due volte, convertilo in lista (o rinegociałlo).
- Complessità eccessiva nelle list comprehension: se hai più di due
foro condizioni, scrivi un ciclo normale con commenti. Il codice deve essere capito da altri (o da te tra 6 mesi). - Ignorare lo scope delle variabili: nelle list comprehension, la variabile del ciclo “perde” nel namespace (Python 2) o è locale (Python 3). In ogni caso, evita di modificare variabili esterne dentro una comprehension.
Una checklist per decidere
- Devo produrre una lista? → List comprehension.
- Devo processare un flusso di dati uno alla volta? → Generatore.
- Voglio solo iterare una volta e fermarmi presto? → Generatore.
- La logica è complessa? → Ciclo normale o funzione generatrice con yield.
- La leggibilità è prioritaria? → Scrivi nel modo più chiaro, anche se più lungo.
In sintesi — cosa fare adesso
Subito. Rivedi il tuo codice Python: cerca cicli for che popolano liste. Sostituisci con list comprehension dove è lineare. Dove possibile, trasforma in generatori per ridurre memoria. Testa le performance con timeit. Impara a leggere gli iterator protocol – è la base delle librerie asincrone e dei framework web. Noi, di Meteora Web, usiamo questi concetti ogni giorno nei nostri progetti Laravel e Python per gestire flussi di dati in modo efficiente, anche su server condivisi. Il risparmio di risorse si traduce in costi inferiori e clienti più soddisfatti.
Se vuoi approfondire l'uso degli iteratori in contesti reali, dai un’occhiata alla documentazione ufficiale Python su iteratori.
Sponsored Protocol