Introduzione
Le comprensioni di lista e i generatori sono due strumenti fondamentali di Python per scrivere codice compatto, leggibile e spesso più efficiente quando si lavora con collezioni di dati. In molti tutorial si parla di come filtrare o trasformare liste, ma c’è un aspetto molto pratico e spesso sottovalutato: come usare queste tecniche per elaborare grandi file di log o dataset riga per riga, evitando di caricare tutto in memoria.
Questo sotto-argomento è utile in scenari reali come:
- analisi di file CSV molto grandi;
- lettura di log applicativi;
- pre-elaborazione di eventi o record provenienti da API;
- costruzione di pipeline di dati semplici ma efficienti.
L’idea chiave è questa: la comprensione di lista produce subito una lista completa in memoria, mentre un generatore produce i valori uno alla volta, solo quando servono. Per piccoli dataset la differenza può sembrare irrilevante; su file grandi, invece, cambia molto in termini di performance e consumo di RAM.
Codice completo
Nel seguente esempio leggiamo un file di log simulato, estraiamo solo le righe utili, trasformiamo i dati e calcoliamo alcune statistiche. Usiamo sia comprensioni di lista sia generatori per mostrare quando conviene uno o l’altro.
from pathlib import Path
from collections import Counter
# File di esempio: ogni riga ha questo formato:
# timestamp|livello|utente|messaggio
#
# Esempio:
# 2026-04-15 10:00:01|INFO|maria|Login riuscito
# 2026-04-15 10:01:12|ERROR|luca|Timeout durante il salvataggio
LOG_FILE = Path("app.log")
def crea_file_di_esempio(percorso: Path) -> None:
"""Crea un file di log di esempio per testare lo script."""
righe = [
"2026-04-15 10:00:01|INFO|maria|Login riuscito",
"2026-04-15 10:01:12|ERROR|luca|Timeout durante il salvataggio",
"2026-04-15 10:02:45|WARNING|anna|Spazio disco quasi esaurito",
"2026-04-15 10:03:10|INFO|maria|Apertura dashboard",
"2026-04-15 10:04:33|ERROR|paolo|Connessione al database fallita",
"2026-04-15 10:05:00|INFO|luca|Logout",
"2026-04-15 10:06:18|ERROR|maria|Errore di validazione input",
]
percorso.write_text("n".join(righe), encoding="utf-8")
def leggi_righe(p: Path):
"""Generatore: restituisce una riga alla volta dal file."""
with p.open("r", encoding="utf-8") as f:
for riga in f:
yield riga.strip()
def parse_log(riga: str):
"""Converte una riga di log in un dizionario."""
timestamp, livello, utente, messaggio = riga.split("|")
return {
"timestamp": timestamp,
"livello": livello,
"utente": utente,
"messaggio": messaggio,
}
def main():
# 1) Prepariamo il file di esempio
crea_file_di_esempio(LOG_FILE)
# 2) Usiamo una comprensione di lista per caricare e parsare tutto.
# Utile se il file è piccolo o se dobbiamo riutilizzare i dati più volte.
record = [parse_log(riga) for riga in leggi_righe(LOG_FILE)]
# 3) Filtriamo con comprensione di lista solo gli errori.
errori = [r for r in record if r["livello"] == "ERROR"]
print("=== Errori trovati ===")
for errore in errori:
print(f´{errore["timestamp"]} - {errore["utente"]}: {errore["messaggio"]}´)
# 4) Estraiamo gli utenti che hanno generato errori usando un set comprehension.
utenti_con_errori = {r["utente"] for r in errori}
print("n=== Utenti coinvolti negli errori ===")
print(utenti_con_errori)
# 5) Calcoliamo il numero di eventi per livello con Counter,
# alimentandolo con un generatore invece di costruire una lista intermedia.
livelli = (parse_log(riga)["livello"] for riga in leggi_righe(LOG_FILE))
conteggio_livelli = Counter(livelli)
print("n=== Conteggio per livello ===")
for livello, count in conteggio_livelli.items():
print(f"{livello}: {count}")
# 6) Esempio pratico: generatore di messaggi di errore "puliti"
# da usare in un report o in una pipeline.
messaggi_errore = (
f´{r["timestamp"]} | {r["utente"]} | {r["messaggio"]}´
for r in (parse_log(riga) for riga in leggi_righe(LOG_FILE))
if r["livello"] == "ERROR"
)
print("n=== Report errori ===")
for msg in messaggi_errore:
print(msg)
if __name__ == "__main__":
main() Spiegazione
Il codice mostra un flusso molto comune nello sviluppo reale: leggere dati, convertirli in una struttura più comoda, filtrare ciò che interessa e produrre un output utile. Vediamo i passaggi principali.
1. Generatore per leggere il file
La funzione leggi_righe usa yield e quindi è un generatore. Invece di restituire tutte le righe in una lista, ne restituisce una alla volta. Questo approccio è ideale quando il file è grande, perché evita di occupare memoria inutilmente.
2. Comprensione di lista per creare una struttura completa
La riga:
record = [parse_log(riga) for riga in leggi_righe(LOG_FILE)] crea una lista di dizionari, uno per ogni riga. È una scelta sensata se sappiamo che dovremo fare più operazioni sui dati, ad esempio filtrare, ordinare e contare. In questo caso, il costo di costruire la lista è compensato dalla comodità di riutilizzarla.
3. Filtraggio con comprensione di lista
La comprensione:
errori = [r for r in record if r["livello"] == "ERROR"] seleziona solo i record di livello ERROR. Qui la comprensione di lista è perfetta perché produce una collezione concreta, pronta da stampare o manipolare ulteriormente.
4. Set comprehension per i valori unici
Usiamo anche una set comprehension:
utenti_con_errori = {r["utente"] for r in errori} per ottenere gli utenti distinti coinvolti negli errori. È un esempio utile perché mostra come le comprensioni non servano solo per le liste, ma anche per ottenere insiemi senza duplicati.
5. Generatori in pipeline
La parte più interessante è questa:
livelli = (parse_log(riga)["livello"] for riga in leggi_righe(LOG_FILE)) Qui non creiamo una lista di livelli, ma un generatore. Il Counter consuma i valori uno alla volta. Questo è un ottimo pattern quando vuoi fare aggregazioni senza materializzare tutti i dati intermedi.
Lo stesso vale per il report finale degli errori:
messaggi_errore = (
f´{r["timestamp"]} | {r["utente"]} | {r["messaggio"]}´
for r in (parse_log(riga) for riga in leggi_righe(LOG_FILE))
if r["livello"] == "ERROR"
) Qui costruiamo una pipeline lazy: leggi riga, fai il parsing, filtra, formatta. Ogni passaggio lavora solo sul dato corrente, senza accumulare tutto in memoria.
Best practice
- Usa le comprensioni di lista quando ti serve una collezione completa e il dataset è di dimensioni gestibili.
- Preferisci i generatori per file grandi, stream di dati o pipeline di elaborazione in cui ogni elemento viene consumato una sola volta.
- Evita comprensioni troppo complesse: se una comprensione supera una o due condizioni e diventa difficile da leggere, meglio riscriverla con un ciclo
foresplicito. - Non riutilizzare un generatore esausto: una volta consumato, non produce più valori. Se ti serve più di una volta, ricrealo.
- Se devi fare più passaggi sugli stessi dati, valuta di convertire il generatore in lista solo quando ha senso dal punto di vista della memoria.
- Separa parsing e logica di business: una funzione come
parse_logrende il codice più testabile e mantenibile.
Riepilogo
Le comprensioni di lista e i generatori non sono solo scorciatoie sintattiche: sono strumenti per scrivere codice più efficiente e più pulito. In particolare, quando lavori con file di log o dataset grandi, i generatori ti permettono di costruire pipeline leggere e scalabili, mentre le comprensioni di lista restano ottime quando vuoi ottenere subito una collezione pronta all’uso.
In sintesi:
- Comprensione di lista = più comoda se ti serve tutto subito in memoria.
- Generatore = più efficiente se vuoi processare dati in modo incrementale.
- Pipeline lazy = soluzione ideale per elaborazioni grandi o ripetitive.
Imparare a scegliere tra questi strumenti è un passo importante per scrivere Python professionale, soprattutto in contesti di analisi dati, automazione e backend.
Approfondisci con risorse ufficiali
- Documentazione Python sulle espressioni generatrici:
https://docs.python.org/3/reference/expressions.html#generator-expressions - Documentazione Python sulle comprensioni:
https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions - Documentazione Python sui generatori e yield:
https://docs.python.org/3/reference/expressions.html#yieldexpr - itertools, modulo utile per pipeline efficienti:
https://docs.python.org/3/library/itertools.html - collections.Counter, per conteggi rapidi:
https://docs.python.org/3/library/collections.html#collections.Counter
