Decoratori Python
Scopri come funzionano i decoratori Python: come scriverli, preservare i metadati con functools.wraps, combinarli e casi d'uso pratici.
Un decoratore è una funzione che avvolge un'altra funzione per estenderne o modificarne il comportamento senza cambiarne il codice sorgente. I decoratori sono una delle funzionalità più potenti e idiomatiche di Python — sono il motore dietro @staticmethod, @classmethod, @property, @functools.lru_cache e molti pattern popolari nei framework web.
Questa pagina spiega come funzionano i decoratori, come scriverne uno da zero, come passare argomenti ai decoratori, come combinarli e quando ciascun pattern è più utile.
Come Funzionano i Decoratori
Un decoratore è semplicemente una funzione che accetta un'altra funzione come argomento e restituisce una nuova funzione. Python fornisce la sintassi @ come abbreviazione per applicarne uno:
@shout
def greet(name):
return f"hello, {name}"Questo è esattamente equivalente a:
def greet(name):
return f"hello, {name}"
greet = shout(greet)La riga @shout dice a Python: dopo aver definito greet, passala immediatamente a shout e riassegna il nome greet a qualunque cosa shout restituisca. Da quel momento in poi, ogni chiamata a greet(...) passa prima per la logica di shout.
Scrivere il Primo Decoratore
Un decoratore di solito definisce una funzione wrapper interna che chiama la funzione originale e aggiunge comportamento extra intorno ad essa:
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@shout
def greet(name):
return f"hello, {name}"
print(greet("world")) # HELLO, WORLD
print(greet("python")) # HELLO, PYTHONwrapper accetta *args e **kwargs in modo da poter inoltrare qualsiasi combinazione di argomenti a func invariata. Questo rende il decoratore compatibile con qualsiasi funzione indipendentemente dalla sua firma — una buona abitudine fin dall'inizio.
Perché il Wrapper Deve Restituire la Funzione Interna
shout termina con return wrapper, non return wrapper(). Questo è intenzionale: shout sta costruendo un nuovo callable, non lo sta ancora chiamando. Se per errore si scrivesse return wrapper(), il decoratore verrebbe eseguito immediatamente al momento della decorazione e greet sarebbe associato al valore restituito da wrapper — una stringa — anziché al callable stesso.
Preservare i Metadati con functools.wraps
Ogni funzione Python porta con sé dei metadati: __name__, __doc__, __module__ e altro. Senza un'attenzione particolare, un decoratore sostituisce la funzione originale con wrapper, perdendo tutto ciò:
def shout(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@shout
def greet(name):
"""Say hello to name."""
return f"hello, {name}"
print(greet.__name__) # wrapper — wrong
print(greet.__doc__) # None — lostSi risolve applicando @functools.wraps(func) al wrapper. Copia i metadati della funzione originale su wrapper:
import functools
def shout(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@shout
def greet(name):
"""Say hello to name."""
return f"hello, {name}"
print(greet("world")) # HELLO, WORLD
print(greet.__name__) # greet
print(greet.__doc__) # Say hello to name.Usa sempre @functools.wraps in qualsiasi decoratore che scrivi. Senza di esso, gli strumenti di debug, i generatori di documentazione e i framework di test vedono il nome della funzione sbagliato. L'unica eccezione è quando si vuole intenzionalmente nascondere l'identità originale.
Esempi Pratici di Decoratori
Logger
Registra ogni chiamata a una funzione con i suoi argomenti e il valore restituito:
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args} kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(3, 5)
# Calling add with args=(3, 5) kwargs={}
# add returned 8Timer
Misura il tempo impiegato da una funzione per essere eseguita:
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__} took {elapsed:.6f}s")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
total = slow_sum(1_000_000)
print(total) # slow_sum took 0.01xxs then 499999500000time.perf_counter() è la scelta giusta qui perché ha la risoluzione più alta disponibile per misurazioni di breve durata.
Memoization (Cache)
Memorizza nella cache il valore restituito per ogni insieme unico di argomenti, così la funzione non viene mai calcolata due volte per lo stesso input:
import functools
def memoize(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # 55
print(fibonacci(30)) # 832040Per il codice in produzione, preferisci il built-in @functools.lru_cache o @functools.cache (Python 3.9+), che gestiscono i casi limite, la thread safety e i limiti di dimensione della cache. La versione fatta a mano è utile per comprendere il pattern.
Controllo degli Accessi
Proteggi una funzione in modo che possa essere eseguita solo quando una condizione è soddisfatta:
import functools
def require_auth(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("is_authenticated"):
raise PermissionError("Authentication required.")
return func(user, *args, **kwargs)
return wrapper
@require_auth
def get_dashboard(user):
return f"Welcome, {user['name']}!"
guest = {"name": "Guest", "is_authenticated": False}
admin = {"name": "Admin", "is_authenticated": True}
try:
print(get_dashboard(guest))
except PermissionError as e:
print(e) # Authentication required.
print(get_dashboard(admin)) # Welcome, Admin!Decoratori con Argomenti
A volte è necessario configurare un decoratore al momento della decorazione — ad esempio, per ripetere una funzione un numero variabile di volte. I decoratori semplici non possono accettare argomenti extra direttamente perché Python passa la funzione, non gli argomenti. La soluzione è una decorator factory: una funzione che accetta la configurazione e restituisce un decoratore:
import functools
def repeat(n):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say(message):
print(message)
say("hello")
# hello
# hello
# helloLeggendo dall'esterno verso l'interno: @repeat(3) chiama prima repeat(3), che restituisce decorator. Python applica quindi decorator a say, che restituisce wrapper. Quindi say finisce per puntare a wrapper — stesso pattern di prima, con il livello extra solo per portare n nello scope.
L'annidamento può sembrare scoraggiante all'inizio. Una scorciatoia mentale: la funzione più esterna contiene la configurazione, quella intermedia contiene la funzione decorata e quella più interna contiene la chiamata intercettata.
Combinare Più Decoratori
È possibile applicare più decoratori a una singola funzione sovrapponendo le righe @. Python li applica dal basso verso l'alto — il decoratore più vicino al def viene applicato per primo:
import functools
def bold(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return "<b>" + func(*args, **kwargs) + "</b>"
return wrapper
def italic(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return "<i>" + func(*args, **kwargs) + "</i>"
return wrapper
@bold
@italic
def greet(name):
return f"Hello, {name}"
print(greet("Alice")) # <b><i>Hello, Alice</i></b>Equivalente a greet = bold(italic(greet)). italic avvolge greet per primo, poi bold avvolge il risultato. L'output mostra che italic viene eseguito più vicino alla stringa grezza e bold avvolge l'esterno.
Decoratori Basati su Classi
Anche una classe può essere un decoratore — qualsiasi oggetto con un metodo __call__ è callable. I decoratori basati su classi sono utili quando il decoratore stesso ha bisogno di mantenere uno stato tra le chiamate:
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Call #{self.count} to {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello()
say_hello()
print(say_hello.count) # 2functools.update_wrapper(self, func) svolge lo stesso compito di @functools.wraps — copia i metadati della funzione originale sull'istanza. Dopo la decorazione, say_hello è un'istanza di CountCalls, quindi say_hello.count è un normale accesso a un attributo.
Quando scegliere una classe rispetto a un decoratore funzione:
- Hai bisogno di uno stato persistente (
count,cache, flag). - Il decoratore ha più metodi o logica ausiliaria.
- Hai bisogno che l'oggetto decorato sia introspezionabile come un tipo specifico.
Errori Comuni con i Decoratori
Dimenticare di chiamare la funzione decorata
Un errore comune all'inizio è restituire il wrapper ma dimenticare di chiamare func al suo interno:
def broken(func):
def wrapper(*args, **kwargs):
print("before")
# forgot to call func!
return wrapperLa funzione decorata restituisce silenziosamente None ogni volta. Assicurati sempre che wrapper chiami func(*args, **kwargs) e restituisca il suo risultato.
Decorare al livello sbagliato
Quando si usano decoratori parametrizzati, dimenticare la chiamata esterna è un errore frequente:
# Wrong — 'repeat' receives the function, not a count
@repeat # should be @repeat(3)
def say(msg):
print(msg)Questo passa say a repeat dove ci si aspetta n, causando un TypeError quando si chiama say.
L'ordine dei decoratori è importante
Con decoratori combinati l'ordine cambia il comportamento. @timer seguito da @log_calls sulla stessa funzione misureranno il tempo della versione già registrata, mentre il contrario registrerà la versione già cronometrata. Pensa bene a cosa vuoi che ogni livello veda.
Relazione con le Closure
La funzione wrapper di un decoratore è una closure — cattura func dallo scope circostante e la mantiene in vita anche dopo che la funzione decoratore esterna ha restituito il controllo. Comprendere le closure rende ovvio il funzionamento interno dei decoratori: l'oggetto cell che contiene func è esattamente ciò che permette a wrapper di chiamare la funzione originale molto dopo che shout(greet) è stato completato.
Per la sintassi *args e **kwargs usata all'interno dei wrapper, consulta il capitolo dedicato. Per le espressioni lambda che si abbinano bene ai decoratori nei pattern di ordine superiore, consulta il capitolo sul lambda.
Riferimento Rapido
| Pattern | Quando usarlo |
|---|---|
wrapper di base | Aggiungere comportamento prima/dopo una funzione |
@functools.wraps | Sempre — preserva __name__, __doc__ |
| Decorator factory (3 livelli) | Necessità di configurare il decoratore |
| Decoratori combinati | Comporre più comportamenti indipendenti |
| Decoratore basato su classe | Necessità di stato persistente tra le chiamate |
@functools.lru_cache | Memoizzare funzioni pure (built-in, pronto per la produzione) |