Storia del problema
I decorator sono uno degli strumenti più potenti di Python, che permettono di modificare il comportamento di funzioni o metodi. A volte è necessario non solo "incapsulare" una funzione, ma configurare il decorator con parametri (argomenti). Questi casi si verificano nel logging, nei controlli di tempo, nelle restrizioni di accesso, ecc.
Problema
I normali decorator accettano solo una funzione, che deve essere incapsulata. Quando è necessario passare parametri al decorator stesso, la sintassi diventa più complessa, il che porta spesso a errori, specialmente con funzioni nidificate e passaggio di *args/**kwargs.
Soluzione
Un decorator con parametri è implementato da una funzione di ordine superiore: prima viene chiamata la funzione "decoratrice" esterna con argomenti, che crea e restituisce il decorator stesso, e il decorator poi riceve la funzione e restituisce l'incapsulamento:
def repeat(n): def decorator(func): def wrapper(*args, **kwargs): result = None for _ in range(n): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def greet(name): print(f"Hello, {name}!") greet("Python") # Output: Hello, Python! (3 volte)
Caratteristiche principali:
Perché non si può implementare un decorator con parametri come un normale decorator?
Se si utilizza @decorator, Python passa la funzione come argomento al decorator. Se si aggiungono le parentesi (@decorator()), Python prima chiama la funzione, e solo il suo risultato viene interpretato come decorator.
def deco(func): # decorator normale: @deco def deco_with_args(arg): # decorator con argomento: @deco_with_args(arg)
In cosa differiscono sostanzialmente i decorator con argomenti dai decorator senza argomenti a livello di chiamata?
I decorator senza argomenti ricevono una funzione come input, mentre i decorator con argomenti non ricevono una funzione, ma parametri, e restituiscono un decorator.
Come applicare correttamente functools.wraps e perché farlo?
functools.wraps(func) nel wrapper mantiene il nome, la stringa di documentazione e altri metadati della funzione originale, altrimenti tutte queste informazioni verrebbero sovrascritte nel wrapper, ostacolando il debug e l'introspezione.
import functools def deco_with_args(arg): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decorator
Implementazione di un decorator senza tenere conto dei parametri o con un numero errato di funzioni nidificate:
def log(level): def wrapper(func): # errore — il wrapper deve essere più profondo print(f"Log: {level}") func() # La funzione non viene restituita come decorator return wrapper @log("INFO") def action(): print("Work!")
Vantaggi:
Svantaggi:
Utilizzo di functools.wraps e corrette funzioni nidificate:
import functools def timer(units): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): import time start = time.time() result = func(*args, **kwargs) end = time.time() if units == 'ms': duration = (end - start) * 1000 else: duration = end - start print(f"Durata: {duration:.4f} {units}") return result return wrapper return decorator @timer('ms') def op(): sum(range(1000)) op()
Vantaggi:
Svantaggi: