Introduzione
Quando si parla di programmazione funzionale in Python, spesso si pensa subito a map, filter, reduce o alle lambda. Tuttavia, un aspetto molto pratico e spesso sottovalutato è l’uso di itertools per costruire pipeline di dati pulite, efficienti e leggibili.
Questo approccio è particolarmente utile quando devi elaborare grandi quantità di dati, applicare trasformazioni in sequenza e mantenere il codice semplice da testare. Invece di scrivere cicli annidati e variabili temporanee, puoi combinare funzioni pure e iteratori per creare flussi di elaborazione chiari.
In questo tutorial vedremo come usare itertools per filtrare, raggruppare e trasformare dati in stile funzionale, con un esempio realistico: l’analisi di una lista di ordini di un e-commerce.
Codice completo
from itertools import chain, groupby
from operator import itemgetter
# Dati di esempio: ordini di un e-commerce
orders = [
{"id": 1, "customer": "Alice", "category": "books", "amount": 24.90, "status": "paid"},
{"id": 2, "customer": "Marco", "category": "tech", "amount": 199.99, "status": "pending"},
{"id": 3, "customer": "Alice", "category": "books", "amount": 15.00, "status": "paid"},
{"id": 4, "customer": "Giulia", "category": "home", "amount": 42.50, "status": "paid"},
{"id": 5, "customer": "Marco", "category": "tech", "amount": 89.90, "status": "paid"},
{"id": 6, "customer": "Sara", "category": "books", "amount": 12.00, "status": "cancelled"},
{"id": 7, "customer": "Giulia", "category": "tech", "amount": 129.00, "status": "paid"},
]
def is_paid(order):
"""Restituisce True solo per gli ordini pagati."""
return order["status"] == "paid"
def get_category(order):
"""Estrae la categoria dell´ordine."""
return order["category"]
def get_customer(order):
"""Estrae il nome del cliente."""
return order["customer"]
def get_amount(order):
"""Estrae l´importo dell´ordine."""
return order["amount"]
# 1) Filtrare solo gli ordini pagati
paid_orders = filter(is_paid, orders)
# 2) Ordinare per categoria prima di usare groupby
# groupby richiede dati ordinati sulla stessa chiave
sorted_paid_orders = sorted(paid_orders, key=get_category)
# 3) Raggruppare gli ordini pagati per categoria
orders_by_category = {
category: list(group)
for category, group in groupby(sorted_paid_orders, key=get_category)
}
# 4) Calcolare il totale per categoria in modo funzionale
totals_by_category = {
category: sum(order["amount"] for order in group)
for category, group in orders_by_category.items()
}
# 5) Trovare tutti i clienti unici che hanno effettuato almeno un acquisto pagato
paid_orders_again = filter(is_paid, orders)
unique_customers = sorted(set(map(get_customer, paid_orders_again)))
# 6) Unire due liste di dati con chain: ordini normali + ordini promozionali
promo_orders = [
{"id": 8, "customer": "Luca", "category": "books", "amount": 9.99, "status": "paid"},
{"id": 9, "customer": "Alice", "category": "home", "amount": 21.00, "status": "paid"},
]
all_paid_orders = list(chain(filter(is_paid, orders), filter(is_paid, promo_orders)))
# 7) Calcolare il totale complessivo di tutti gli ordini pagati
total_revenue = sum(order["amount"] for order in all_paid_orders)
print("Ordini pagati raggruppati per categoria:")
for category, group in orders_by_category.items():
print(category, group)
print("nTotali per categoria:")
for category, total in totals_by_category.items():
print(f"{category}: {total:.2f} €")
print("nClienti unici:")
print(unique_customers)
print("nRicavi totali:")
print(f"{total_revenue:.2f} €") Spiegazione
Il cuore di questo esempio è la costruzione di una pipeline di elaborazione. Ogni passaggio prende un input e produce un output senza modificare i dati originali. Questo è molto vicino allo stile funzionale.
1. filter per selezionare i dati rilevanti
La funzione filter restituisce un iteratore con gli elementi che soddisfano una condizione. Nel nostro caso, teniamo solo gli ordini con stato paid.
Questo è utile perché separa chiaramente la logica di selezione dalla logica di trasformazione.
2. sorted + groupby per raggruppare
La funzione itertools.groupby è potente, ma ha una regola importante: funziona correttamente solo se i dati sono già ordinati per la stessa chiave di raggruppamento. Per questo prima usiamo sorted(..., key=get_category).
Una volta ordinati gli ordini, groupby crea gruppi consecutivi per categoria. Questo è molto utile per report, statistiche e aggregazioni.
3. Funzioni pure per rendere il codice più leggibile
Le funzioni is_paid, get_category, get_customer e get_amount sono semplici, prevedibili e riutilizzabili. Separare queste piccole funzioni aiuta a:
- testare meglio il codice;
- riusare la stessa logica in più punti;
- rendere più chiaro il significato di ogni trasformazione.
4. map e set per ottenere elementi unici
Per estrarre i clienti che hanno acquistato, usiamo map(get_customer, ...). Questo restituisce i nomi dei clienti di tutti gli ordini pagati. Poi set elimina i duplicati e sorted rende il risultato ordinato.
Questa è una tipica operazione funzionale: trasformare una sequenza di dati in un’altra sequenza più utile, senza effetti collaterali.
5. chain per unire iterabili
itertools.chain permette di concatenare più iterabili senza creare liste intermedie inutili. È molto utile quando vuoi trattare più sorgenti di dati come un unico flusso.
Nel codice abbiamo unito gli ordini normali e quelli promozionali, filtrando entrambi solo per gli ordini pagati.
6. Generatori e consumo lazy
Molte funzioni usate qui restituiscono iteratori, non liste complete. Questo significa che i dati vengono elaborati lazy, cioè solo quando servono. Il vantaggio è duplice:
- meno memoria usata;
- pipeline più efficienti su dataset grandi.
In programmazione funzionale questo è molto interessante, perché favorisce composizione e streaming dei dati.
Best practice
- Preferisci funzioni piccole e pure: una funzione dovrebbe fare una cosa sola e senza modificare lo stato esterno.
- Usa nomi descrittivi: funzioni come is_paid o get_category spiegano subito la loro intenzione.
- Ricorda l’ordinamento prima di groupby: senza sorted, il raggruppamento può produrre risultati inattesi.
- Evita conversioni inutili in lista: conserva gli iteratori quando possibile per migliorare le prestazioni.
- Componi le operazioni: filtra, trasforma e aggrega in passaggi chiari invece di scrivere un unico blocco monolitico.
- Usa operator.itemgetter quando possibile: per chiavi semplici può essere più leggibile e spesso più efficiente di una lambda.
Un errore comune è confondere il codice “compatto” con il codice “funzionale”. Un’espressione troppo densa può diventare difficile da mantenere. L’obiettivo non è scrivere meno righe, ma costruire un flusso di elaborazione chiaro e affidabile.
Riepilogo
In questo tutorial abbiamo visto un uso molto pratico della programmazione funzionale in Python: sfruttare itertools per creare pipeline di trasformazione dei dati.
Abbiamo imparato a:
- filtrare elementi con filter;
- estrarre informazioni con map;
- raggruppare dati con sorted e groupby;
- unire sequenze con chain;
- scrivere funzioni pure e riutilizzabili.
Questo stile è molto utile in contesti reali come reportistica, analisi dati, ETL leggero e gestione di collezioni complesse. Se vuoi migliorare il tuo codice Python, imparare a pensare in termini di trasformazioni di dati è un passo fondamentale.
Approfondisci con risorse ufficiali
- itertools nella documentazione ufficiale Python: https://docs.python.org/3/library/itertools.html
- functools nella documentazione ufficiale Python: https://docs.python.org/3/library/functools.html
- operator nella documentazione ufficiale Python: https://docs.python.org/3/library/operator.html
- Guida ai iteratori e alle iterable: https://docs.python.org/3/glossary.html#term-iterable
