Ti è mai capitato di copiare un blocco di codice, incollarlo tre volte cambiando solo un numero, e una settimana dopo scoprire un bug… in tutte e tre le copie? Quel momento — correggere lo stesso errore in tre posti — è esattamente il problema che le funzioni risolvono. Una funzione è un pezzo di logica scritto una volta sola, con un nome, che richiami quando ti serve. E in Python le funzioni hanno superpoteri che in questo tutorial smontiamo uno per uno: argomenti con default, *args, **kwargs e le lambda. Sono le sigle che vedi in ogni firma di scikit-learn e che troppi tutorial danno per scontate.
Tipi, strutture dati, controllo di flusso e funzioni: le basi di Python sono nel libro, in un percorso che si legge in ordine.
Prerequisiti: Python 3.12 in un ambiente virtuale (python -m venv .venv o uv venv, poi attivalo) e un editor come VS Code o PyCharm. Se non ricordi cos’è una lista o un dizionario, ti basta sapere che la prima è una sequenza ordinata di valori ([1, 2, 3]) e il secondo una mappa chiave→valore ({"nome": "Ada"}): sono i due contenitori che gli asterischi impacchettano e spacchettano.
La funzione come scatola: input, lavoro, output
L’analogia più onesta per una funzione è una macchina del caffè: metti dentro capsula e acqua (gli argomenti), lei fa il suo lavoro internamente (il corpo), ti restituisce il caffè (il return). Non ti interessa cosa succede dentro: ti fidi dell’interfaccia. Questo è il patto: dati gli stessi input, ottieni lo stesso output.
# Una funzione è una scatola con un contratto chiaro
def prezzo_scontato(prezzo, sconto=0.10):
"""Applica uno sconto (default 10%) e restituisce il prezzo finale."""
finale = prezzo * (1 - sconto)
return round(finale, 2)
print(prezzo_scontato(100)) # 90.0 — usa il default
print(prezzo_scontato(100, 0.25)) # 75.0 — posizionale
print(prezzo_scontato(prezzo=100, sconto=0.25)) # 75.0 — per nome (keyword)
Tre modi di chiamarla, un’unica scatola. Gli argomenti posizionali vengono abbinati in ordine; quelli keyword per nome, e puoi scriverli in qualsiasi ordine. Il valore di default (sconto=0.10) rende il parametro opzionale. Una trappola da conoscere subito: mai usare un oggetto mutabile come default (def f(x, acc=[])): la lista viene creata una volta sola e condivisa tra tutte le chiamate. Usa acc=None e creala dentro la funzione.
La funzione come scatola: gli argomenti entrano da sinistra (con eventuali default se mancano), il corpo lavora al riparo dal mondo esterno, e il return è l’unico canale d’uscita ufficiale.
*args: la scatola che accetta quanti pacchi arrivano
Finora la nostra scatola aveva ingressi fissi. Ma come fa print() ad accettare uno, tre o dieci valori? Con *args. L’asterisco davanti a un parametro dice: «raccogli tutti gli argomenti posizionali extra in una tupla«. Pensa al cestino della raccolta indifferenziata sotto la scrivania: tutto ciò che non ha un posto assegnato finisce lì, in ordine di arrivo.
def media(*valori):
"""Accetta un numero qualsiasi di valori e ne calcola la media."""
if not valori: # tupla vuota = nessun argomento
raise ValueError("Serve almeno un valore")
return sum(valori) / len(valori)
print(media(4, 8)) # 6.0 — dentro la funzione: valori = (4, 8)
print(media(1, 2, 3, 4, 5)) # 3.0 — valori = (1, 2, 3, 4, 5)
# L'asterisco funziona anche al contrario: SPACCHETTA una lista
voti = [27, 30, 24, 28]
print(media(*voti)) # 27.25 — come scrivere media(27, 30, 24, 28)
Quest’ultimo trucco — l’asterisco nella chiamata — è lo stesso simbolo con il ruolo opposto: nella definizione impacchetta, nella chiamata spacchetta. Tenerlo a mente ti risparmia metà della confusione.
**kwargs: il pacco con l’etichetta
Il doppio asterisco fa la stessa cosa ma con gli argomenti keyword: li raccoglie in un dizionario. Se *args è il cestino dell’indifferenziata, **kwargs è l’armadietto con le etichette: ogni cosa arriva con il suo nome attaccato.
def configura_modello(nome, **opzioni):
"""Simula la firma di un estimatore scikit-learn."""
print(f"Modello: {nome}")
for chiave, valore in opzioni.items():
print(f" {chiave} = {valore}")
configura_modello("RandomForest", n_estimators=200, max_depth=8)
# Modello: RandomForest
# n_estimators = 200
# max_depth = 8
# E anche qui: ** nella chiamata spacchetta un dizionario
config = {"n_estimators": 500, "max_depth": 12}
configura_modello("RandomForest", **config)
Adesso sai leggere le firme che incontri ovunque nel machine learning: quando la documentazione di scikit-learn scrive Pipeline.fit(X, y, **params) o vedi def __init__(self, *args, **kwargs) in una classe, non c’è nessuna magia — solo una tupla e un dizionario che raccolgono gli extra. È anche il meccanismo con cui una funzione «inoltra» argomenti a un’altra senza doverli conoscere tutti.
*args e **kwargs non sono sintassi esoterica: sono una tupla e un dizionario con l’asterisco davanti. Tutto qui. Il 90% della paura svanisce quando li stampi.
lambda: la funzione usa-e-getta
A volte ti serve una funzione così piccola che darle un nome è più costoso che scriverla. La lambda è il bicchiere di carta delle funzioni: lo usi una volta, dove ti serve, e non lo lavi. Sintassi: lambda argomenti: espressione — una sola espressione, il cui valore è il return implicito.
modelli = [
{"nome": "knn", "accuratezza": 0.87},
{"nome": "albero", "accuratezza": 0.91},
{"nome": "logistica", "accuratezza": 0.89},
]
# Ordina per accuratezza decrescente: la lambda dice a sorted DOVE guardare
classifica = sorted(modelli, key=lambda m: m["accuratezza"], reverse=True)
print(classifica[0]["nome"]) # albero
# Equivalente con def — identico, solo più verboso
def per_accuratezza(m):
return m["accuratezza"]
classifica = sorted(modelli, key=per_accuratezza, reverse=True)
Il posto naturale delle lambda è come argomento key di sorted, min e max, o dentro pandas.DataFrame.apply. Onestà dovuta: se la tua lambda supera una riga, ha bisogno di un if annidato o la usi in più punti, non usare una lambda. Scrivi un def con un nome parlante: il te stesso del futuro, che debugga alle 23, ringrazierà. E per le trasformazioni numeriche su intere colonne, ricordati che NumPy e pandas hanno quasi sempre un’operazione vettorizzata più veloce di qualsiasi apply con lambda.
Prova tu
Costruisci un file report.py con una funzione report(*numeri, decimali=2, **statistiche) che: (1) calcola sempre media e massimo dei numeri ricevuti; (2) arrotonda ai decimali richiesti; (3) per ogni coppia in statistiche — ad esempio soglia=25 — stampa quanti numeri superano quel valore. Chiamala in tre modi: report(10, 20, 30), report(*[5, 15, 25, 35], decimali=1), e con un dizionario spacchettato report(1, 2, 3, **{"soglia": 2}). Se tutti e tre funzionano senza modificare la funzione, hai capito impacchettamento e spacchettamento.
Per approfondire: la sezione «More on Defining Functions» del tutorial ufficiale copre anche i parametri positional-only (/) e keyword-only (*) che qui abbiamo solo sfiorato; e il capitolo 7 di Fluent Python di Luciano Ramalho (2ª edizione) tratta le funzioni come oggetti di prima classe — il concetto che rende possibile passare una lambda a sorted.

I commenti sono riservati agli iscritti.
Accedi per commentare