Comprensioni di lista e generatori per lavorare con grandi insiemi di dati in Python

by theArchitect
SHARE
Comprensioni di lista e generatori per lavorare con grandi insiemi di dati in Python
© Guida-HTML5.it

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 for esplicito.
  • 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_log rende 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

SHARE