Introduzione
In Python una funzione non deve necessariamente ricevere soltanto dati primitivi come numeri o stringhe. Può ricevere anche un’altra funzione come argomento e utilizzarla per modificare il proprio comportamento. Questo approccio è particolarmente utile quando vogliamo separare la logica generale di un’operazione dai dettagli specifici.
Una funzione passata come argomento viene spesso chiamata callback. La funzione principale si occupa del flusso generale, mentre la callback decide come elaborare ogni elemento. In questo modo il codice diventa più riutilizzabile: invece di scrivere una funzione diversa per ogni caso, possiamo scriverne una sola e fornirle strategie differenti.
In questo tutorial realizzeremo un piccolo sistema per elaborare una lista di prodotti. La funzione principale riceverà:
- una collezione di prodotti;
- una funzione per calcolare il prezzo finale;
- una funzione per decidere se un prodotto deve essere incluso.
La funzione restituirà una nuova lista di dizionari contenente i risultati elaborati. L’esempio mostra quindi come progettare parametri flessibili e valori di ritorno strutturati senza ricorrere a variabili globali o duplicazione del codice.
Codice completo
from typing import Callable, TypedDict
class Prodotto(TypedDict):
nome: str
prezzo: float
categoria: str
class Risultato(TypedDict):
nome: str
prezzo_originale: float
prezzo_finale: float
def applica_sconto(prezzo: float) -> float:
"""Riduce il prezzo del 20%."""
return round(prezzo * 0.80, 2)
def nessuno_sconto(prezzo: float) -> float:
"""Restituisce il prezzo senza modifiche."""
return prezzo
def solo_elettronica(prodotto: Prodotto) -> bool:
"""Seleziona soltanto i prodotti di elettronica."""
return prodotto["categoria"] == "elettronica"
def prezzo_superiore_a_50(prodotto: Prodotto) -> bool:
"""Seleziona i prodotti con prezzo originale superiore a 50 euro."""
return prodotto["prezzo"] > 50
def elabora_prodotti(
prodotti: list[Prodotto],
calcola_prezzo: Callable[[float], float],
seleziona: Callable[[Prodotto], bool],
) -> list[Risultato]:
"""
Filtra i prodotti e calcola il prezzo finale di quelli selezionati.
"""
risultati: list[Risultato] = []
for prodotto in prodotti:
if seleziona(prodotto):
prezzo_originale = prodotto["prezzo"]
prezzo_finale = calcola_prezzo(prezzo_originale)
risultati.append({
"nome": prodotto["nome"],
"prezzo_originale": prezzo_originale,
"prezzo_finale": prezzo_finale,
})
return risultati
catalogo: list[Prodotto] = [
{
"nome": "Tastiera meccanica",
"prezzo": 89.90,
"categoria": "elettronica",
},
{
"nome": "Lampada da scrivania",
"prezzo": 35.50,
"categoria": "casa",
},
{
"nome": "Cuffie wireless",
"prezzo": 64.99,
"categoria": "elettronica",
},
]
risultati = elabora_prodotti(
catalogo,
calcola_prezzo=applica_sconto,
seleziona=solo_elettronica,
)
for risultato in risultati:
print(
f"{risultato[´nome´]}: "
f"{risultato[´prezzo_finale´]:.2f} euro"
) Spiegazione
Funzioni come valori
In Python le funzioni sono oggetti di prima classe. Questo significa che possono essere assegnate a una variabile, inserite in una lista oppure passate come argomento a un’altra funzione. Quando passiamo applica_sconto, senza parentesi, stiamo passando l’oggetto funzione. Se invece scrivessimo applica_sconto(), la funzione verrebbe eseguita immediatamente e il suo risultato diventerebbe l’argomento.
La firma di elabora_prodotti contiene tre parametri. Il primo, prodotti, è una lista di elementi. Il secondo, calcola_prezzo, è una callback che riceve un float e restituisce un altro float. Il terzo, seleziona, riceve un prodotto e restituisce un valore booleano.
La notazione Callable[[float], float] documenta il contratto del parametro: la callback deve accettare un numero decimale e restituire un numero decimale. Analogamente, Callable[[Prodotto], bool] indica una funzione di selezione. Questi suggerimenti sono utili per gli editor e per strumenti come mypy, anche se Python non li impone automaticamente durante l’esecuzione.
Parametri e valori di ritorno
La funzione non modifica direttamente la lista originale. Crea invece una nuova lista chiamata risultati e la restituisce con return. Questa scelta riduce gli effetti collaterali e rende il comportamento più prevedibile: dopo la chiamata, catalogo contiene ancora i dati iniziali.
Ogni elemento restituito è un dizionario con tre informazioni: nome, prezzo originale e prezzo finale. Un valore di ritorno strutturato è spesso più leggibile rispetto a una tupla non documentata come (nome, prezzo, sconto). In un progetto reale potremmo sostituire il dizionario con una dataclass se l’oggetto dovesse avere molti comportamenti.
Riutilizzare la stessa funzione
Possiamo cambiare il risultato senza modificare elabora_prodotti. Per esempio, possiamo selezionare i prodotti sopra i 50 euro e non applicare sconti:
risultati = elabora_prodotti(
catalogo,
calcola_prezzo=nessuno_sconto,
seleziona=prezzo_superiore_a_50,
) La funzione principale rimane identica, ma il comportamento cambia grazie ai parametri funzione. Questa tecnica è comune nei sistemi di pagamento, nei filtri di ricerca, nei processi di importazione dati e nei test automatici.
Best practice
- Definire contratti chiari: usa annotazioni di tipo come
Callableper descrivere gli argomenti accettati e i valori restituiti. - Dare nomi espliciti alle callback: nomi come
calcola_prezzoeselezionasono più comprensibili di nomi generici comefofunc. - Non modificare inutilmente gli argomenti: restituire una nuova lista evita sorprese per chi invoca la funzione.
- Separare le responsabilità: una callback dovrebbe occuparsi di una sola decisione, ad esempio filtrare oppure calcolare.
- Controllare il contratto delle callback: se una callback restituisce un tipo inatteso, l’errore potrebbe comparire più avanti. I type checker aiutano a individuare questi problemi prima dell’esecuzione.
- Usare funzioni lambda solo per casi brevi: una lambda può essere utile per una trasformazione semplice, ma una funzione con nome è preferibile quando la logica è importante o riutilizzabile.
- Documentare il valore di ritorno: una docstring deve spiegare cosa contiene il risultato e se l’input originale viene modificato.
Per esempio, una lambda può essere adatta a una regola molto breve:
risultati = elabora_prodotti(
catalogo,
calcola_prezzo=lambda prezzo: round(prezzo * 0.90, 2),
seleziona=lambda prodotto: prodotto["categoria"] == "casa",
) Se la lambda dovesse crescere, è meglio trasformarla in una funzione con un nome descrittivo. In questo modo il codice è più semplice da testare e da mantenere.
Riepilogo
Passare funzioni come argomenti permette di costruire funzioni generiche e riutilizzabili. La funzione principale definisce il processo, mentre le callback personalizzano le singole decisioni. I parametri possono quindi rappresentare non solo dati, ma anche comportamenti.
Un buon design prevede firme leggibili, annotazioni di tipo, callback con responsabilità limitate e valori di ritorno strutturati. Nel nostro esempio, la stessa funzione ha potuto filtrare prodotti e calcolare prezzi diversi senza duplicare la logica del ciclo.
