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.
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: 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