Introduzione
Uno degli aspetti più utili della programmazione funzionale in Python è la composizione di funzioni: invece di scrivere una lunga sequenza di passaggi mescolati tra loro, si costruisce una pipeline di trasformazioni piccole, pure e riutilizzabili. Questo approccio rende il codice più leggibile, più facile da testare e spesso più semplice da estendere nel tempo.
La composizione è particolarmente efficace quando devi elaborare dati in più fasi: pulizia, validazione, trasformazione e formattazione finale. Invece di creare una funzione enorme che fa tutto, separi ogni responsabilità in una funzione dedicata e poi le colleghi tra loro.
In questo tutorial vedremo un caso pratico: elaborare una lista di ordini e ottenere un report sintetico. L’obiettivo non è solo “far funzionare” il codice, ma farlo in modo elegante, riusabile e coerente con i principi della programmazione funzionale.
Codice completo
from functools import reduce
from datetime import datetime
# 1) Funzioni piccole e pure
def normalize_customer_name(name: str) -> str:
"""Rimuove spazi superflui e uniforma il formato del nome."""
return " ".join(name.strip().split()).title()
def is_valid_order(order: dict) -> bool:
"""Verifica che l´ordine abbia i campi minimi richiesti."""
required_keys = {"customer", "amount", "date", "status"}
return required_keys.issubset(order.keys()) and order["amount"] > 0
def is_completed(order: dict) -> bool:
"""Filtra solo gli ordini completati."""
return order["status"] == "completed"
def parse_date(date_str: str) -> datetime:
"""Converte una data in formato ISO in oggetto datetime."""
return datetime.fromisoformat(date_str)
def enrich_order(order: dict) -> dict:
"""Aggiunge informazioni derivate senza modificare l´input."""
return {
**order,
"customer": normalize_customer_name(order["customer"]),
"date_obj": parse_date(order["date"]),
"month": parse_date(order["date"]).strftime("%Y-%m")
}
def format_report_line(order: dict) -> str:
"""Formatta una singola riga del report."""
return f"{order[´month´]} | {order[´customer´]:<20} | €{order[´amount´]:.2f}"
def compose(*functions):
"""
Crea una funzione composta.
compose(f, g, h)(x) equivale a f(g(h(x)))
"""
def inner(value):
return reduce(lambda acc, fn: fn(acc), reversed(functions), value)
return inner
# 2) Pipeline di elaborazione
orders = [
{"customer": " mario rossi ", "amount": 120.50, "date": "2026-01-12", "status": "completed"},
{"customer": "anna verdi", "amount": 0, "date": "2026-01-13", "status": "completed"},
{"customer": "luca bianchi", "amount": 89.90, "date": "2026-01-14", "status": "pending"},
{"customer": "giulia neri", "amount": 240.00, "date": "2026-02-02", "status": "completed"},
{"customer": " elena gialli", "amount": 75.00, "date": "2026-02-05", "status": "completed"},
]
# Pipeline funzionale: filtra, arricchisce, formatta
process_order = compose(
format_report_line,
enrich_order
)
valid_completed_orders = list(
map(
process_order,
filter(
lambda order: is_valid_order(order) and is_completed(order),
orders
)
)
)
# 3) Aggregazione finale con reduce
total_revenue = reduce(
lambda acc, order: acc + order["amount"],
filter(lambda order: is_valid_order(order) and is_completed(order), orders),
0.0
)
# 4) Output
print("REPORT ORDINI COMPLETATI")
print("-" * 40)
for line in valid_completed_orders:
print(line)
print("-" * 40)
print(f"Totale incasso: €{total_revenue:.2f}") Spiegazione
Il codice è costruito attorno a un principio fondamentale: ogni funzione fa una cosa sola. Questo rende la pipeline più facile da capire e da modificare.
1. Funzioni pure e riusabili
Le funzioni come normalize_customer_name, is_valid_order, is_completed e parse_date non modificano stati esterni e producono sempre lo stesso risultato a parità di input. Sono quindi funzioni pure, ideali per la programmazione funzionale.
Per esempio, normalize_customer_name pulisce il nome del cliente, eliminando spazi extra e convertendo il testo in formato leggibile. Questo tipo di funzione è molto utile in contesti reali, dove i dati in ingresso sono spesso sporchi o incoerenti.
2. Arricchimento dei dati senza mutazione
La funzione enrich_order crea un nuovo dizionario invece di modificare quello originale. Questo è un comportamento importante in stile funzionale: evita effetti collaterali e rende il flusso dei dati più prevedibile.
Notare che il dizionario restituito contiene sia i campi originali sia nuovi campi derivati, come date_obj e month. In un progetto reale, questo approccio è utile quando vuoi aggiungere metadati per report, analisi o esportazioni.
3. Composizione di funzioni
La funzione compose permette di concatenare più funzioni in una sola. Nel nostro esempio:
process_order = compose(format_report_line, enrich_order)
significa che prima l’ordine viene arricchito e poi trasformato in una stringa pronta per il report. Il vantaggio è che puoi leggere la pipeline come una sequenza logica di trasformazioni.
4. Filter e map insieme
La combinazione di filter e map è un classico della programmazione funzionale:
- filter seleziona solo gli ordini validi e completati
- map applica la trasformazione
process_ordera ogni elemento selezionato
Questo approccio è molto più espressivo di un ciclo for pieno di condizioni e append manuali, soprattutto quando la logica cresce.
5. Aggregazione con reduce
Per calcolare il totale degli incassi usiamo reduce, che accumula progressivamente il valore di tutti gli ordini completati. Anche se in molti casi una somma semplice potrebbe essere fatta con sum(), reduce diventa utile quando l’aggregazione è più articolata o richiede logica personalizzata.
Nel nostro caso il risultato finale è un report leggibile e un totale economico coerente con i dati filtrati.
Best practice
- Preferisci funzioni piccole e nominate bene: una funzione con un solo compito è più facile da testare e riutilizzare.
- Evita mutazioni inutili: restituisci nuovi oggetti invece di modificare quelli passati come argomento.
- Usa la composizione per costruire pipeline leggibili: quando i passaggi sono chiari, il codice diventa più manutenibile.
- Non abusare di lambda complesse: se una logica cresce, trasformala in una funzione dedicata con nome esplicito.
- Separa validazione, trasformazione e output: sono responsabilità diverse e vanno tenute distinte.
- Attenzione ai dati duplicati: nel nostro esempio
parse_dateviene chiamata due volte; in un progetto reale potresti ottimizzare memorizzando il risultato in una variabile locale nella funzione di arricchimento. - Usa type hints e docstring: migliorano la leggibilità e aiutano sia il team sia gli strumenti di analisi statica.
Riepilogo
La composizione di funzioni è una tecnica centrale nella programmazione funzionale in Python. Ti permette di costruire flussi di elaborazione chiari, modulari e facili da estendere. Nel tutorial abbiamo visto come:
- scrivere funzioni pure per validazione e trasformazione;
- arricchire i dati senza mutare gli input;
- comporre funzioni con una pipeline riutilizzabile;
- usare
filter,mapereducein modo coerente; - produrre un report finale pulito e leggibile.
Se lavori spesso con dati, log, record o file CSV/JSON, questa tecnica può migliorare molto la qualità del tuo codice. Il vantaggio principale non è solo estetico: è la possibilità di ragionare sul programma come su una sequenza di trasformazioni ben definite.
Approfondisci con risorse ufficiali
- functools — documentazione ufficiale Python:
https://docs.python.org/3/library/functools.html - itertools — strumenti per iterazioni efficienti:
https://docs.python.org/3/library/itertools.html - datetime — gestione delle date e degli orari:
https://docs.python.org/3/library/datetime.html - PEP 8 — guida di stile del codice Python:
https://peps.python.org/pep-0008/
