Enumerate, zip, generatori, map e filter: il flusso pigro dei dati

Smetti di scrivere for i in range(len(lista)): Python ha strumenti nati per scorrere i dati con eleganza. Enumerate, zip, generatori, map e filter spiegati con esempi eseguibili e un'idea chiave: la pigrizia come virtù.

Hai mai scritto for i in range(len(lista)) solo perché ti serviva l’indice insieme al valore? E poi magari, per scorrere due liste in parallelo, hai usato quell’indice per pescare da entrambe? Funziona, certo. Ma è come guidare in prima per tutto il viaggio: arrivi, però il motore soffre e chi ti guarda capisce che c’è qualcosa che non va. Python ha una marcia più fluida per scorrere i dati, e in questa lezione la impariamo pezzo per pezzo: enumerate, zip, i generatori, map e filter.

Tipi, strutture dati, controllo di flusso e funzioni: le basi di Python sono nel libro, in un percorso che si legge in ordine.

Leggi «Le basi di Python» nel libro →

Prima di partire, un richiamo veloce: se non ricordi cos’è un iterabile, ti basta sapere che è qualunque oggetto su cui puoi fare un ciclo for — liste, stringhe, dizionari, file aperti. Tutto quello che vediamo oggi lavora sugli iterabili. Ambiente consigliato: Python 3.12 in un ambiente virtuale (con python -m venv .venv o, se vuoi lo strumento più moderno, uv venv), e un editor come VS Code o PyCharm.

enumerate: l’indice senza contarlo a mano

Immagina di fare l’appello in classe: non dici solo i nomi, dici «numero 1, Aluisi; numero 2, Bianchi». Il numero e il nome viaggiano insieme. enumerate fa esattamente questo: prende un iterabile e ti restituisce coppie (indice, valore), senza che tu debba gestire nessun contatore.

# Appello: indice e nome insieme, senza range(len(...))
studenti = ["Aluisi", "Bianchi", "Corradi", "De Santis"]

for numero, nome in enumerate(studenti, start=1):
    print(f"{numero}. {nome}")

# 1. Aluisi
# 2. Bianchi
# 3. Corradi
# 4. De Santis

Nota il parametro start=1: di default enumerate parte da 0, ma per un elenco leggibile da umani spesso vuoi partire da 1. Piccolo dettaglio, grande pulizia.

zip: due liste che camminano mano nella mano

Il nome non è casuale: zip funziona come una cerniera lampo. Hai due file di dentini — due liste — e la cerniera li accoppia uno a uno mentre la tiri. Se una lista è più corta, zip si ferma dove finisce la più corta, senza errori.

# Voti e nomi in due liste separate: zip li accoppia
nomi = ["Aluisi", "Bianchi", "Corradi"]
voti = [27, 30, 24]

for nome, voto in zip(nomi, voti):
    print(f"{nome}: {voto}")

# Vuoi essere sicuro che le liste abbiano la stessa lunghezza?
# Da Python 3.10 esiste strict=True: solleva errore se non combaciano.
for nome, voto in zip(nomi, voti, strict=True):
    pass  # se le lunghezze differiscono, qui ottieni un ValueError

# Trucco classico: zip(*coppie) "apre la cerniera" e separa di nuovo
coppie = list(zip(nomi, voti))
nomi2, voti2 = zip(*coppie)
print(nomi2)  # ('Aluisi', 'Bianchi', 'Corradi')

Usa strict=True ogni volta che due liste devono avere la stessa lunghezza — per esempio feature e label di un dataset: meglio un errore subito che un modello addestrato su dati sfalsati.

I generatori: la pigrizia come virtù

Qui arriva l’idea più importante della lezione. Una lista è come cucinare cento pizze in anticipo e ammucchiarle sul bancone: occupi spazio anche se i clienti ne mangeranno dieci. Un generatore è il pizzaiolo che inforna una pizza solo quando qualcuno la ordina: produce un valore alla volta, su richiesta, e non tiene niente in memoria oltre a quello che serve per fare il prossimo.

Si scrive come una funzione normale, ma al posto di return usa yield: ogni yield consegna un valore e mette in pausa la funzione, che riparte da lì alla richiesta successiva.

# Un generatore di quadrati: nessuna lista in memoria
def quadrati(n):
    for i in range(n):
        yield i * i  # consegna un valore e si mette in pausa

gen = quadrati(1_000_000_000)  # un miliardo di quadrati... istantaneo!
print(next(gen))  # 0
print(next(gen))  # 1
print(next(gen))  # 4  -> calcola solo quando glielo chiedi

# Versione compatta: la generator expression (parentesi tonde, non quadre)
somma = sum(x * x for x in range(10_000_000))
print(somma)  # nessuna lista da 10 milioni di elementi è mai esistita

La riga quadrati(1_000_000_000) è istantanea proprio perché non calcola nulla: costruisce solo la «ricetta». Questo si chiama valutazione pigra (lazy evaluation), ed è il motivo per cui puoi processare un file di log da 10 GB su un portatile con 8 GB di RAM: leggi una riga, la elabori, la dimentichi.

Una lista risponde alla domanda «quali sono tutti i valori?». Un generatore risponde a una domanda più umile e più potente: «qual è il prossimo?».

Attenzione al rovescio della medaglia: un generatore si consuma. Una volta scorso fino in fondo è vuoto, non puoi ripercorrerlo né chiedergli len() o un elemento a caso con [5]. Se ti serve rileggere i dati o accedervi per indice, la lista resta la scelta giusta. Onestamente: per una lista di 50 elementi il generatore non ti fa risparmiare niente di percepibile — usalo quando i dati sono tanti o arrivano da una sorgente esterna.

map e filter: trasformare e setacciare

Se non ricordi cosa sia una funzione lambda, ti basta sapere che è una funzione usa-e-getta scritta in una riga: lambda x: x * 2 è una funzione senza nome che raddoppia il suo argomento.

map applica una funzione a ogni elemento di un iterabile, come un timbro che passa su ogni foglio di una risma. filter invece è un setaccio: tiene solo gli elementi per cui la funzione risponde True. Entrambi, dettaglio cruciale, restituiscono iteratori pigri — non liste: anche loro producono un valore alla volta.

temperature_f = [98.6, 101.2, 99.5, 103.1, 97.9]

# map: converte tutte le temperature da Fahrenheit a Celsius
celsius = map(lambda f: round((f - 32) * 5 / 9, 1), temperature_f)

# filter: tiene solo le temperature sopra i 38 gradi (febbre)
febbre = filter(lambda c: c > 38, celsius)

print(list(febbre))  # [38.4, 39.5]

# La stessa cosa, in stile "Pythonic" moderno: una generator expression
febbre2 = (round((f - 32) * 5 / 9, 1) for f in temperature_f
           if (f - 32) * 5 / 9 > 38)
print(list(febbre2))  # [38.4, 39.5]

Onestà intellettuale: nella comunità Python di oggi map e filter con le lambda sono spesso considerati meno leggibili delle comprehension, e persino Guido van Rossum preferisce le seconde. Conoscerli però resta indispensabile: li incontrerai in codice altrui, e con una funzione già pronta per nome — map(str.strip, righe) — sono ancora eleganti e chiari.

Tutto insieme: una pipeline pigra

Il bello è che questi pezzi si incastrano in catena, come i macchinari di una catena di montaggio dove ogni stazione lavora un pezzo alla volta e lo passa alla successiva. Ecco una pipeline completa che analizza un file di misurazioni senza mai caricarlo tutto in memoria:

# Pipeline pigra: dal file al risultato, un elemento alla volta.
# Crea prima il file di prova, poi processalo.
from pathlib import Path

Path("sensori.csv").write_text(
    "sensore_a,21.5\nsensore_b,38.9\nsensore_a,22.1\n"
    "sensore_c,41.0\nsensore_b,19.8\n", encoding="utf-8"
)

def leggi_righe(percorso):
    """Generatore: consegna una riga alla volta, il file non sta mai
    tutto in memoria."""
    with open(percorso, encoding="utf-8") as f:
        for riga in f:
            yield riga.strip()

righe = leggi_righe("sensori.csv")                       # generatore
record = (r.split(",") for r in righe)                    # ancora pigro
letture = ((nome, float(val)) for nome, val in record)    # conversione
allarmi = (coppia for coppia in letture if coppia[1] > 35) # setaccio

# Fino a qui NON è stato letto neanche un byte del file!
# Il lavoro parte solo ora, quando enumerate "tira" i valori:
for i, (nome, valore) in enumerate(allarmi, start=1):
    print(f"Allarme {i}: {nome} = {valore}°C")

# Allarme 1: sensore_b = 38.9°C
# Allarme 2: sensore_c = 41.0°C

Rileggi il commento a metà: quando costruisci la catena non succede nulla. È il ciclo finale a «tirare» i dati attraverso tutte le stazioni, un elemento per volta. Questo schema — sorgente, trasformazioni pigre, un consumatore che tira — è lo stesso che ritroverai nei DataLoader di PyTorch e nelle pipeline di dati che incontrerai più avanti nel corso.

Il flusso pigro dei dati sorgente file / lista generatore yield map trasforma filter setaccia for consuma un solo elemento in transito alla volta: niente viene calcolato prima che serva la richiesta («dammi il prossimo») risale la catena all’indietro

Il flusso pigro: le trasformazioni sono collegate in catena, ma è il consumatore finale a «tirare» i dati, un elemento alla volta. La richiesta viaggia all’indietro, il dato in avanti.

Prova tu

Scrivi un generatore numeri_pari(limite) che produce i numeri pari da 0 a limite. Poi costruisci una pipeline pigra che: eleva ogni pari al quadrato (con una generator expression o map), tiene solo i quadrati che finiscono per 4 (con filter o un if) e infine stampa i primi dieci risultati numerati con enumerate(..., start=1). Verifica con un test onesto: aggiungi una print("calcolo", x) dentro il generatore e osserva quando viene eseguita davvero. Se compare solo durante il ciclo finale, hai capito la pigrizia.

Per approfondire: il capitolo 17 di Fluent Python (2ª edizione) di Luciano Ramalho è la miglior trattazione in circolazione su iteratori e generatori; per una referenza rapida e gratuita, la pagina ufficiale itertools della documentazione Python ti mostra quanto lontano può arrivare questo stile: chain, islice, batched e compagnia sono tutti costruiti sull’idea che hai imparato oggi.

I commenti sono riservati agli iscritti.

Accedi per commentare