I decoratori sono funzioni che prendono un'altra funzione come argomento allo scopo di estendere o sostituire il comportamento della funzione ricevuta.
Se la funzione A è un decoratore e la funzione B è il suo argomento, allora la funzione A modifica, estende o sostituisce il comportamento della funzione B senza modificarne il codice.
Diciamo che la funzione decoratrice A avvolge la funzione B.
Anche se parliamo di «modificare» il comportamento, la funzione avvolta non viene realmente cambiata.
Il comportamento viene aggiunto attorno alla funzione avvolta (e a ciò che restituisce), oppure il comportamento della funzione avvolta viene sostituito con un altro comportamento.
Una funzione di ordine superiore è una funzione che accetta una o più funzioni come argomenti e/o restituisce una o più funzioni. Una funzione usata come argomento, o restituita da un'altra funzione, è una funzione di prima classe. Una funzione Python, in quanto oggetto chiamabile, è una funzione di prima classe che può essere memorizzata in una variabile o usata come argomento, proprio come qualsiasi altro valore o oggetto. Le funzioni di ordine superiore e le funzioni di prima classe lavorano insieme per rendere possibili i decoratori.
Il simbolo @ viene anteposto al nome della funzione decoratrice e posizionato appena sopra la funzione da decorare, così:
@decorator
def decorated_function():
pass
Alcuni decoratori accettano argomenti:
@decorator_with_arg(name="Bob")
def decorated_function2():
pass
Se un decoratore prevede argomenti predefiniti, devi usare le parentesi nella chiamata @decorator() perché il decoratore funzioni, come faresti nel chiamare qualsiasi funzione:
@decorator_with_default_arg()
def decorated_function3():
pass
Se un decoratore accetta argomenti posizionali, non fornirli produrrà un errore simile a questo:
TypeError: decorator_with_pos_arg() missing 1 required positional argument: 'name'
La sintassi @decorator è zucchero sintattico, una scorciatoia per chiamare la funzione decoratrice e passarle come argomento la funzione decorata.
Di seguito ci sono esempi di modi alternativi per chiamare un decoratore:
def function():
pass
function = decorator(function)
def function2():
pass
function2 = decorator_with_arg(name="Bob")(function2)
def function3():
pass
function3 = decorator_with_default_arg()(function3)
La maggior parte dei decoratori ha lo scopo di estendere o sostituire il comportamento di un'altra funzione, ma alcuni decoratori possono limitarsi a restituire le funzioni che avvolgono.
I decoratori sono funzioni che prendono almeno un argomento: la funzione che avvolgono. Di solito restituiscono la funzione avvolta o il risultato di un'espressione che usa la funzione avvolta.
Un semplice decoratore, uno che si limita a restituire la funzione che avvolge, si può scrivere così:
>>> def do_nothing(func):
... return func
...
... @do_nothing
... def function4():
... return 4
...
>>> print(function4())
4
Un decoratore può anche limitarsi ad aggiungere effetti collaterali, come informazioni aggiuntive usate per il logging:
>>> def my_logger(func):
... print(f"Entering {func.__name__}")
... return func
...
... @my_logger
... def my_func():
... print("Hello")
...
>>> my_func()
Entering my_func
Hello
Un decoratore non restituisce se stesso. Può restituire i suoi argomenti di funzione, un'altra funzione, o uno o più valori che sostituiscono il valore restituito dalla funzione ricevuta o decorata. Se un decoratore restituisce un'altra funzione, di solito restituirà una funzione interna.
Una funzione può essere definita all'interno di un'altra funzione. Una funzione annidata di questo tipo si chiama funzione interna. Un decoratore può usare una funzione interna per avvolgere il proprio argomento di funzione. Il decoratore restituisce poi la sua funzione interna. La funzione interna può quindi restituire l'argomento di funzione originale.
Di seguito c'è un esempio di decoratore usato per la validazione:
def my_validator(func):
def my_wrapper(world):
print(f"Entering {func.__name__} with {world} argument")
if world == "Pluto":
print("Pluto is not a planet!")
else:
return func(world)
return my_wrapper
@my_validator
def my_func(planet):
print(f"Hello, {planet}!")
my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!
my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Pluto is not a planet!
Sulla prima riga abbiamo la definizione dell'argomento func del decoratore.
Sulla riga successiva c'è la definizione della funzione interna del decoratore, che avvolge l'argomento func.
Poiché la funzione interna avvolge l'argomento func del decoratore, riceve lo stesso argomento che viene passato a func.
Nota che il wrapper non deve per forza usare lo stesso nome per l'argomento definito in func.
La funzione originale usa planet e il decoratore usa world: il decoratore funziona comunque.
La funzione interna restituisce func oppure, se world == "Pluto", stampa che Plutone non è un pianeta.
In alternativa, la si potrebbe scrivere in modo che sollevi un ValueError.
Quindi la funzione interna avvolge func e restituisce func oppure fa qualcosa che sostituisce ciò che func farebbe.
Il decoratore restituisce la sua funzione interna.
La funzione interna può restituire o meno la funzione originale ricevuta.
A seconda del codice che viene eseguito condizionalmente nella funzione wrapper o nella funzione interna, func può essere restituita, può essere sollevato un errore, oppure può essere restituito un valore del tipo restituito da func.
Si possono scrivere decoratori per funzioni che accettano un numero arbitrario di argomenti usando la sintassi *args e **kwargs.
Di seguito c'è un esempio di decoratore per una funzione che accetta un numero arbitrario di argomenti:
def double(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs) * 2
return wrapper
@double
def add(*args):
return sum(args)
print(add(2, 3, 4))
#-> 18
print(add(2, 3, 4, 5, 6))
#-> 40
Questo funziona per raddoppiare il valore restituito dalla funzione argomento. Se vogliamo triplicare, quadruplicare, ecc. il valore restituito, possiamo aggiungere un parametro al decoratore stesso, come mostriamo nella sezione successiva.
Di seguito c'è un esempio di decoratore che può essere configurato per moltiplicare il valore restituito dalla funzione decorata per una quantità arbitraria:
def multi(factor=1):
if factor == 0:
raise ValueError("factor must not be 0")
def outer_wrapper(func):
def inner_wrapper(*args, **kwargs):
return func(*args, **kwargs) * factor
return inner_wrapper
return outer_wrapper
@multi(factor=3)
def add(*args):
return sum(args)
print(add(2, 3, 4))
#-> 27
print(add(2, 3, 4, 5, 6))
#-> 60
Le prime righe verificano che factor non sia 0.
Poi viene definito il wrapper esterno.
Questo ha la stessa firma che ci aspettiamo per un decoratore senza parametri.
Il wrapper esterno ha una funzione interna con la stessa firma della funzione originale.
Il wrapper interno svolge il lavoro di moltiplicare il valore restituito dalla funzione originale per l'argomento passato al decoratore.
Il wrapper esterno restituisce il wrapper interno, e il decoratore restituisce il wrapper esterno.
Di seguito c'è un esempio di decoratore parametrizzato che controlla se validare o meno l'argomento passato alla funzione originale:
def check_for_pluto(check=True):
def my_validator(func):
def my_wrapper(world):
print(f"Entering {func.__name__} with {world} argument")
if check and world == "Pluto":
print("Pluto is not a planet!")
else:
return func(world)
return my_wrapper
return my_validator
@check_for_pluto(check=False)
def my_func(planet):
print(f"Hello, {planet}!")
my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!
my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Hello, Pluto!
Questo permette di azionare facilmente il controllo di Pluto, senza dover modificare my_func.