Introduzione
Una funzione Python non è soltanto un blocco di istruzioni che può essere chiamato con delle parentesi. In Python, una funzione è anche un oggetto: può essere assegnata a una variabile, inserita in una lista, passata come argomento a un’altra funzione e restituita come risultato. Questa caratteristica permette di costruire programmi più flessibili e riutilizzabili.
Un caso pratico molto importante è l’uso delle callback. Una callback è una funzione passata a un’altra funzione, che la chiamerà al momento opportuno. Questo approccio è comune nei programmi che elaborano dati, nei sistemi basati su eventi e nelle librerie che richiedono comportamenti personalizzati.
In questo tutorial costruiremo un piccolo sistema per filtrare e trasformare una lista di prodotti. Invece di creare una funzione diversa per ogni possibile elaborazione, definiremo una funzione generale che riceve altre funzioni come argomenti.
Codice completo
from typing import Callable
def filtra_prodotti(
prodotti: list[dict],
criterio: Callable[[dict], bool]
) -> list[dict]:
"""Restituisce i prodotti che rispettano il criterio indicato."""
return [prodotto for prodotto in prodotti if criterio(prodotto)]
def applica_sconto(
prodotti: list[dict],
calcola_nuovo_prezzo: Callable[[float], float]
) -> list[dict]:
"""Crea una nuova lista applicando una funzione al prezzo."""
return [
{
**prodotto,
"prezzo": calcola_nuovo_prezzo(prodotto["prezzo"])
}
for prodotto in prodotti
]
def prodotto_disponibile(prodotto: dict) -> bool:
"""Controlla se un prodotto è disponibile."""
return prodotto["quantita"] > 0
def prezzo_scontato(prezzo: float) -> float:
"""Riduce il prezzo del 20% e lo arrotonda a due decimali."""
return round(prezzo * 0.80, 2)
prodotti = [
{"nome": "Tastiera", "prezzo": 49.90, "quantita": 12},
{"nome": "Mouse", "prezzo": 24.50, "quantita": 0},
{"nome": "Monitor", "prezzo": 189.99, "quantita": 4},
]
disponibili = filtra_prodotti(prodotti, prodotto_disponibile)
in_offerta = applica_sconto(disponibili, prezzo_scontato)
for prodotto in in_offerta:
print(
f"{prodotto[´nome´]}: "
f"{prodotto[´prezzo´]:.2f} euro"
) Spiegazione
Definire una funzione che riceve un’altra funzione
La funzione filtra_prodotti() riceve due argomenti: la lista dei prodotti e il criterio da applicare. Il parametro criterio contiene un riferimento a una funzione.
def filtra_prodotti(prodotti, criterio):
return [prodotto for prodotto in prodotti if criterio(prodotto)] All’interno della list comprehension, l’espressione criterio(prodotto) esegue la funzione ricevuta. Il risultato deve essere booleano: se è True, il prodotto viene inserito nella nuova lista.
La funzione prodotto_disponibile() rappresenta il comportamento concreto:
def prodotto_disponibile(prodotto):
return prodotto["quantita"] > 0 La chiamata completa è la seguente:
disponibili = filtra_prodotti(prodotti, prodotto_disponibile) È importante notare che passiamo prodotto_disponibile senza parentesi. Scrivendo prodotto_disponibile(), infatti, chiameremmo subito la funzione senza fornire l’argomento richiesto. Senza parentesi, invece, passiamo alla funzione filtra_prodotti() il riferimento all’oggetto funzione.
Usare una funzione per trasformare i dati
La funzione applica_sconto() segue lo stesso principio, ma usa una callback per trasformare ogni prezzo. Il parametro calcola_nuovo_prezzo deve essere una funzione che riceve un numero decimale e restituisce un altro numero decimale.
def applica_sconto(prodotti, calcola_nuovo_prezzo):
return [
{
**prodotto,
"prezzo": calcola_nuovo_prezzo(prodotto["prezzo"])
}
for prodotto in prodotti
] L’operatore **prodotto copia le coppie chiave-valore del dizionario originale. In questo modo viene creata una nuova struttura, senza modificare direttamente la lista iniziale. Questa scelta evita effetti collaterali difficili da individuare.
Annotazioni di tipo con Callable
L’importazione Callable dal modulo typing rende più comprensibile il contratto della funzione:
criterio: Callable[[dict], bool] Questa annotazione significa che criterio deve essere una funzione che riceve un dizionario e restituisce un valore booleano. Analogamente:
calcola_nuovo_prezzo: Callable[[float], float] indica una funzione che riceve un float e restituisce un float. Le annotazioni non vengono normalmente usate da Python per bloccare automaticamente gli errori durante l’esecuzione, ma aiutano l’IDE, gli strumenti di controllo statico e gli altri sviluppatori.
Passare una funzione anonima
Quando il comportamento è breve e viene utilizzato una sola volta, è possibile passare una funzione lambda direttamente:
prodotti_costosi = filtra_prodotti(
prodotti,
lambda prodotto: prodotto["prezzo"] > 100
) La callback controlla se il prezzo supera 100 euro. Per comportamenti più lunghi o riutilizzabili è preferibile definire una funzione con def, perché il nome rende il codice più leggibile.
Best practice
- Dai un nome descrittivo alle callback: nomi come
prodotto_disponibilespiegano immediatamente il loro scopo. - Definisci un contratto chiaro: indica quali dati riceve la funzione e quale risultato deve restituire.
- Evita modifiche inaspettate: quando possibile, crea nuove liste o dizionari invece di cambiare direttamente gli oggetti ricevuti.
- Usa le annotazioni di tipo:
Callabledocumenta il tipo di callback atteso e facilita la manutenzione. - Preferisci funzioni nominate per la logica complessa: le
lambdasono adatte a espressioni brevi, non a procedure articolate. - Progetta funzioni con una sola responsabilità: una funzione dovrebbe filtrare, trasformare o calcolare, evitando di svolgere troppe attività contemporaneamente.
- Considera la testabilità: una callback separata può essere testata indipendentemente dalla funzione che la utilizza.
Riepilogo
In Python è possibile passare una funzione a un’altra funzione proprio come si passa un numero, una stringa o una lista. Questo meccanismo consente di separare il processo generale dal comportamento specifico.
Nel nostro esempio, filtra_prodotti() sa come attraversare una lista, ma non decide in anticipo quali prodotti selezionare. La decisione viene delegata alla callback. Allo stesso modo, applica_sconto() sa come costruire la nuova lista, mentre la funzione ricevuta stabilisce come calcolare il nuovo prezzo.
Questo stile rende il codice più modulare, estendibile e semplice da testare. Quando devi eseguire la stessa procedura con regole diverse, valuta se una funzione come callback può evitare duplicazioni e rendere il progetto più flessibile.
