I decoratori in Python sono particolari Funzioni che prendono in argomento altre funzioni, aggiungono qualche tipo di funzionalità a monte e a valle di queste (wrapping), e restituiscono la risultante funzione modificata.
def uppercase_decorator(func): # decoratore
def wrapper(): # funzione wrapper
original_result = func()
modified_result = original_result.upper()
return modified_result
return wrapper
def greet():
return "Ciao Mondo!"
greet = uppercase_decorator(greet) # applicazione manuale del decoratore
print(greet()) # Output: CIAO MONDO!L’utilità dei decoratori è data dal fatto che il linguaggio stesso ne mette a supporto una sintassi specifica:
def uppercase_decorator(func): # decoratore
def wrapper(): # funzione wrapper
original_result = func()
modified_result = original_result.upper()
return modified_result
return wrapper
@uppercase_decorator # applicazione automatica del decoratore
def greet():
return "Ciao Mondo!"
print(greet()) # Output: CIAO MONDO!La sintassi dei decoratori “svolge” le funzioni indicate dalla sintassi a chiocciola @ fornendo come argomento la funzione immediatamente seguente finché non si ottiene una nuova funzione, appunto, decorata. Si possono applicare i decoratori anche alle classi.
Si possono inoltre accodare più decoratori uno dopo l’altro, combinandone gli effetti (il secondo decoratore prende la funzione decorata dal primo, e così via).
Le best practice nell’uso dei decoratori (che capiremo dopo aver letto le seguenti sezioni) saranno quindi:
- Usare
functools.wrapsper preservare i metadati originali della funzione; - Mantenere i decorator semplici e focalizzati su una singola responsabilità;
- Documentare come il decorator modifica il comportamento della funzione;
- Considerare i potenziali impatti sulle prestazioni;
- Usare i decorator integrati quando appropriato invece di crearne di personalizzati (cosa che vale sempre anche per le Eccezioni).
Decoratori con argomenti
I decoratori possono supportare argomenti (notiamo, gli argomenti della funzione decorata) attraverso il *args, **kwargs della classica Sintassi ad asterisco, che prende tutti gli argomenti in formato lista e in formato dizionario (i cosiddetti keyword arguments).
def uppercase_decorator(func):
def wrapper(*args, **kwargs):
original_result = func(*args, **kwargs)
return original_result.upper()
return wrapper
@uppercase_decorator
def greet(name):
return f"Ciao {name}!"
print(greet("Imane")) # Output: CIAO IMANE!I decoratori possono quindi accettare i loro stessi parametri (non quelli della funzione decorata) utilizzando un ulteriore livello di annidamento, secondo la sintassi (un po’ convoluta):
def repeat(n=1): # wrapper del decoratore
def decorator(func): # decoratore
@functools.wraps(func)
def wrapper(*args, **kwargs): # funzione wrapper
result = None
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(n=3) # chiama repeat(n=3)
# -> restituisce decorator(say_hello) con n=3 nella closure
def say_hello():
print("Ciao")
return "Done"
say_hello() # Output: Ciao Ciao CiaoPreservazione dei metadati
Di default, la funzione wrapper di un decoratore (quella che effettivamente viene restituita) è una funzione a se stante che chiama la funzione originale (quella che viene decorata). In questo modo, i metadati (nome, documentazione, ecc…) della funzione originale vengono sovrascritti.
Visto che questo potrebbe essere non desiderabile, il linguaggio mette a disposizione nel modulo functools un’utilità per preservare i metadati originali, che è a sua volta un decoratore:
import functools
def uppercase_decorator(func):
@functools.wraps(func) # preserva i metadati
def wrapper(*args, **kwargs):
original_result = func(*args, **kwargs)
return original_result.upper()
return wrapperDecoratori di classe
Come abbiamo anticipato, si possono applicare i decoratori anche alle classi (vedere Oggetti). Questo permette di modificarne il comportamento introducendo nuovi metodi in maniera programmatica:
def add_greeting(cls):
cls.greet = lambda self: f"Ciao, sono {self.name}"
return cls
@add_greeting
class Person:
def __init__(self, name):
self.name = name
p = Person("Luca")
print(p.greet()) # Output: Ciao, sono LucaNotiamo che anche classi di tipo Callable, cioè che implementano il metodo call, possono essere usate a mo’ di decoratori (e in tal caso dovranno accettare la funzione da decorare come argomento del costruttore).
Python include diversi decoratori di default per quanto riguarda i membri di classe, che vanno a sostituire quelle che negli altri linguaggi sono i membri statici sono funzionalità come le proprietà con getter/setter, i membri statici, ecc…
@property: converte un metodo in una proprietà, e permette di specificare getter e setter come@func.gettere@func.setter;@classmethod: passa come primo argomento ad una funzione la definizione di classe stessa anziché l’istanza di classe su cui viene chiamata. Effettivamente rappresenta la cosa più vicina in Python che abbiamo ai metodi statici;@staticmethod: contrariamente al nome, non rappresenta un metodo statico vero e proprio, ma solo una funzione che non accetta niente di relativo alla classe (né l’istanza specifica né la definizione) come primo argomento. Ha senso solo per raggruppare il codice;@abstractmethod: segnala un metodo di classe come astratto (da specializzare in una sottoclasse). La chiamata del metodo astratto, cioè nella classe base non specializzato, comporta che automaticamente venga sollevata un’eccezione.