Decoratori in Python: che cosa sono, come funzionano e come scriverne uno
Un decoratore in Python è una funzione che riceve un'altra funzione e restituisce una nuova funzione, di solito un involucro che aggiunge un comportamento prima o dopo la chiamata: logging, misura del tempo, cache, controllo dei permessi. La scrittura @decoratore sopra def è una scorciatoia per func = decoratore(func); per conservare il nome e la docstring della funzione originale si usa functools.wraps.
Un decoratore in Python è una funzione che riceve un'altra funzione e ne restituisce una nuova, di solito un involucro intorno a quella originale. In questo modo si aggiunge un comportamento — logging, misura del tempo, cache, ripetizione dei tentativi — senza cambiare il codice della funzione. I decoratori si scrivono con la riga @nome sopra la dichiarazione della funzione e si incontrano ovunque: in Flask e FastAPI, in pytest, nella libreria standard.
Che cos'è un decoratore in parole semplici
Pensate a un regalo e alla carta da regalo. Il regalo (la funzione) resta lo stesso, ma intorno compare uno strato che entra in azione per primo. Il decoratore è proprio questo involucro: quando la funzione viene chiamata, per prima cosa esegue il codice dell'involucro, che richiama la funzione originale e può fare qualcosa con il suo risultato.
Funziona perché in Python le funzioni sono oggetti normali. Si possono passare ad altre funzioni, restituire da funzioni e dichiarare dentro altre funzioni. Su queste tre proprietà si basano i decoratori.
Come funziona un decoratore: la @ è una scorciatoia
Prendiamo un decoratore semplicissimo, che stampa un messaggio prima e dopo la chiamata:
def log_calls(func):
def wrapper():
print(f"Chiamo {func.__name__}")
result = func()
print(f"{func.__name__} completata")
return result
return wrapper
@log_calls
def hello():
print("Ciao!")
hello()
Chiamo hello
Ciao!
hello completata
La scrittura @log_calls sopra def è del tutto equivalente a questa riga, scritta dopo la dichiarazione:
def hello():
print("Ciao!")
hello = log_calls(hello)
Ora il nome hello punta a wrapper, mentre la funzione originale vive dentro la closure: wrapper ricorda la variabile func della funzione esterna. Un dettaglio importante: il decoratore viene applicato una sola volta, nel momento in cui viene eseguito il def, cioè all'import del modulo; l'involucro invece entra in azione a ogni chiamata.
Closure e decoratori
Una closure è una funzione annidata che ricorda le variabili della funzione esterna anche dopo che questa è terminata. Senza le closure i decoratori non esisterebbero: è così che wrapper accede a func. Dentro una closure si può conservare anche uno stato, per esempio un contatore di chiamate:
def count_calls(func):
calls = 0
def wrapper(*args, **kwargs):
nonlocal calls
calls += 1
print(f"{func.__name__}: chiamata n. {calls}")
return func(*args, **kwargs)
return wrapper
Come scrivere un decoratore in Python: schema universale
Il primo esempio ha due difetti: l'involucro non accetta argomenti e «cancella» il nome della funzione originale. Lo schema funzionante, da cui conviene partire per qualsiasi decoratore, è questo:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# codice prima della chiamata
result = func(*args, **kwargs)
# codice dopo la chiamata
return result
return wrapper
Tre regole dello schema:
*args, **kwargs— l'involucro accetta qualsiasi argomento e lo inoltra senza modifiche.return result— non dimenticate di restituire il risultato, altrimenti la funzione decorata restituirà sempreNone.@functools.wraps(func)— trasferisce i metadati della funzione originale all'involucro.
Esempio di decoratore: misura del tempo di esecuzione:
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} ha impiegato {elapsed:.3f} s")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
slow_sum(10_000_000) # slow_sum ha impiegato 0.142 s
A che cosa serve functools.wraps
Senza wraps la funzione decorata perde il nome, la docstring e gli altri attributi: all'esterno si vede wrapper:
def plain(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@plain
def add(a, b):
"""Somma due numeri."""
return a + b
print(add.__name__, add.__doc__) # wrapper None
Con @functools.wraps(func) l'involucro riceve __name__, __doc__, __module__, __qualname__, __annotations__ della funzione originale, e nell'attributo __wrapped__ resta il riferimento all'originale. È importante per il debug e per i traceback, per help(), per i framework che registrano le funzioni per nome e per inspect.signature, che mostra la firma reale.
Decoratore con parametri (con argomenti)
A volte il decoratore ha bisogno di impostazioni: @retry(times=3), @app.route("/"). Per questo si aggiunge un livello di annidamento in più. La funzione esterna accetta i parametri e restituisce il vero decoratore: questa costruzione si chiama fabbrica di decoratori.
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def ping():
print("ping")
ping() # ping, ping, ping — tre righe
Ordine delle chiamate: repeat(times=3) restituisce decorator, che viene applicato a ping e restituisce wrapper. In altre parole, @repeat(times=3) equivale a ping = repeat(times=3)(ping).
Un esempio pratico: ripetere la chiamata in caso di errore:
import functools
import time
def retry(times=3, delay=1.0, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except exceptions:
if attempt == times:
raise
time.sleep(delay)
return wrapper
return decorator
@retry(times=5, delay=0.5, exceptions=(ConnectionError,))
def fetch_rate():
...
Più decoratori sulla stessa funzione
I decoratori si possono impilare uno sopra l'altro. Vengono applicati dal basso verso l'alto: quello più vicino a def è il primo.
@timer
@log_calls
def report():
...
# equivale a:
report = timer(log_calls(report))
Durante la chiamata lo strato esterno (timer) entra in azione per primo, quindi misurerà anche il tempo di lavoro di log_calls. L'ordine conta: per esempio, se un decoratore verifica i permessi e un altro mette in cache il risultato, la sequenza cambia il comportamento.
Decoratori standard e integrati di Python
Molti decoratori utili sono già nel linguaggio o nella libreria standard:
| Decoratore | Che cosa fa |
|---|---|
@property | trasforma un metodo in un attributo di sola lettura; con @x.setter diventa un attributo controllato |
@staticmethod | metodo di classe senza self, una normale funzione nello spazio dei nomi della classe |
@classmethod | il metodo riceve la classe cls invece dell'istanza — così si scrivono i costruttori alternativi |
@functools.cache, @functools.lru_cache | memorizzano i risultati in base agli argomenti |
@functools.wraps | copia i metadati della funzione sull'involucro |
@dataclasses.dataclass | genera __init__, __repr__, __eq__ per la classe |
@contextlib.contextmanager | trasforma un generatore in un gestore di contesto per with |
Cache: functools.cache e lru_cache
Un decoratore di cache ricorda il risultato per ogni insieme di argomenti e, alla chiamata successiva con gli stessi argomenti, lo restituisce senza ricalcolarlo. L'esempio classico sono i numeri di Fibonacci ricorsivi, che senza cache richiedono un tempo esponenziale:
import functools
@functools.cache
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(100)) # 354224848179261915075
functools.cache (comparso in Python 3.9) conserva tutti i risultati senza limiti. @functools.lru_cache(maxsize=128) tiene solo le ultime voci ed elimina le più vecchie: è più sicuro per i servizi che girano a lungo. Le statistiche si leggono con fib.cache_info(), la cache si svuota con fib.cache_clear(). Gli argomenti di queste funzioni devono essere hashable: una lista come argomento genera TypeError: unhashable type: 'list'. Inoltre la cache ha senso solo per le funzioni pure, cioè quelle che con gli stessi argomenti restituiscono sempre lo stesso risultato e non hanno effetti collaterali.
Che cosa fa il decoratore property
@property permette di usare un metodo come un attributo, senza parentesi, e di controllare l'assegnazione se serve:
class Account:
def __init__(self, balance):
self._balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("Il saldo non può essere negativo")
self._balance = value
acc = Account(100)
print(acc.balance) # 100
acc.balance = -5 # ValueError
staticmethod e classmethod
class Money:
def __init__(self, cents):
self.cents = cents
@classmethod
def from_euros(cls, euros):
return cls(round(euros * 100))
@staticmethod
def is_valid(value):
return value >= 0
m = Money.from_euros(12.5)
print(m.cents, Money.is_valid(-1)) # 1250 False
Proprio @classmethod permette al metodo di lavorare con la classe stessa e non con le sue istanze: from_euros crea correttamente anche un oggetto di una sottoclasse.
Decoratori di classe e classi usate come decoratori
Un decoratore si può applicare a una classe: riceve la classe e restituisce una classe (la stessa o modificata). È così che funziona @dataclass. Un esempio semplice è un registro di classi:
registry = {}
def register(cls):
registry[cls.__name__] = cls
return cls
@register
class CsvExporter:
pass
print(registry) # {'CsvExporter': <class '__main__.CsvExporter'>}
Il caso inverso è una classe usata come decoratore. Va bene qualsiasi oggetto con il metodo __call__; lo stato è comodo da conservare negli attributi dell'istanza:
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.calls = 0
def __call__(self, *args, **kwargs):
self.calls += 1
return self.func(*args, **kwargs)
@CountCalls
def tick():
pass
tick(); tick()
print(tick.calls) # 2
Decoratore asincrono
Per le funzioni async def anche l'involucro deve essere una corrutina e attendere il risultato con await; altrimenti restituirebbe un oggetto corrutina non eseguito:
import functools
def log_async(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
print(f"Avvio {func.__name__}")
result = await func(*args, **kwargs)
print(f"Fatto {func.__name__}")
return result
return wrapper
A che cosa servono i decoratori in Python
- Logica trasversale — logging, metriche, misura dei tempi senza copiare il codice in ogni funzione.
- Cache —
@functools.cacheper i calcoli costosi e puri. - Controlli — permessi, validazione degli argomenti, autorizzazione nei gestori web.
- Registrazione — rotte in Flask e FastAPI (
@app.get("/")), fixture di pytest. - Affidabilità — ripetizioni dei tentativi, timeout, gestione degli errori.
Il vantaggio principale è la separazione delle responsabilità: la funzione si occupa del proprio compito, mentre il comportamento accessorio si aggiunge con una sola riga e si rimuove con la stessa facilità.
Errori tipici
- L'involucro non restituisce il risultato — dopo la decorazione la funzione restituisce
None. - Manca
*args, **kwargs—TypeErrorquando si chiama la funzione con argomenti. - Dimenticato
wraps— nomi e docstring sbagliati, confusione nei log. - Parentesi della fabbrica —
@repeatal posto di@repeat(3): la funzione finisce nel parametrotimes. - Codice pesante nel corpo del decoratore — viene eseguito all'import del modulo, non alla chiamata.
Conclusioni
I decoratori in Python sono funzioni di ordine superiore: ricevono una funzione, restituiscono un involucro, e la @ è solo una forma breve di assegnazione. Partite dallo schema con *args, **kwargs, return result e functools.wraps; per le impostazioni aggiungete il livello della fabbrica. Funzioni, closure e annotazioni, su cui si reggono i decoratori, sono trattati in pratica nel corso di Python.
Domande frequenti
Si può rimuovere un decoratore da una funzione?
Se il decoratore usa functools.wraps, l'originale è accessibile tramite l'attributo __wrapped__: func.__wrapped__(...) chiama la funzione senza involucro. Altrimenti bisogna eliminare la riga con la @ dal codice.
Quando viene eseguito il codice del decoratore?
Il corpo del decoratore viene eseguito una sola volta, al momento della dichiarazione della funzione, di solito durante l'import del modulo. Il codice dentro wrapper viene eseguito a ogni chiamata della funzione decorata.
Si possono decorare i metodi di una classe?
Sì, un decoratore normale con *args, **kwargs funziona anche con i metodi: self semplicemente diventa il primo elemento di args.
In che cosa un decoratore si differenzia dall'ereditarietà?
L'ereditarietà modifica il comportamento di una classe attraverso una gerarchia, mentre il decoratore avvolge dall'esterno una funzione o una classe specifica e si combina facilmente con altri decoratori senza toccare il codice.
Questo argomento è trattato in modo approfondito, con pratica, nel corso «Python 3.15: от новичка до профи».
Vai al corso