Python 3 - Lezioni su Concetti Intermedi

12 - I Decoratori

I decoratori sono uno strumento che ci consente di estendere e modificare il comportamento di funzioni e classi senza doverne alterare direttamente il codice sorgente.

Abbiamo già visto alcuni decoratori in azione nella serie sulla Programmazione a Oggetti, I decoratori @staticmethod() e @classmethod() che come ricorderete consentono ai nostri metodi di trascendere quello che è il comportamento tradizionale dei metodi diciamo standard, rendendolo più appropriato in certi determinati contesti.

Uno degli ambienti in cui mi trovo a lavorare spesso con I decoratori in Python è quello dei Web Framework: Django ad esempio fa un uso fantastico di questo strumento in molti di quei casi in cui le nostre funzioni hanno bisogno di alcune funzionalità extra, che sono tuttavia abbastanza generiche e quindi utilizzabili su più funzioni. Il decoratore @login_required() ad esempio, è uno di questi.

I decoratori sono molto potenti quindi, anche per il fatto che una volta definiti, possono essere utilizzati su più funzioni, rispettando in questa maniera il precetto DRY, Don’t Repeat Yourself, che tradotto significa: evita di scrivere lo stesso codice più volte di quanto non sia strettamente necessario.

Una definizione un po più rigorosa di decoratore è: una funzione che prende come parametro un’altra funzione, aggiunge delle funzionalità e restituisce un’altra funzione, senza appunto, alterare il codice sorgente della funzione passata come parametro.

Questo è possibile per il fatto che in Python, le funzioni sono First Class Objects il che significa in maniera molto sintetica, che possono essere passate come parametro e restituite come qualsiasi altro valore, definite all’interno di altre funzioni (nel qual caso si parla di funzioni annidate) e assegnate a delle variabili.

def funzione_decoratore(funzione_parametro): 
    def wrapper(): 
        """ nome convenzionale - wrapper significa 'incarto, confezione' """
        print("... codice da eseguire prima di 'funzione_parametro' ...") 					
        funzione_parametro()
        print("... codice da eseguire dopo di 'funzione_parametro' ...") 
    return wrapper

def mia_funzione(): 
    print("Hello World!")
mia_funzione = funzione_decoratore(mia_funzione) 

mia_funzione()
# output:

... codice da eseguire prima di funzione_parametro ...
Hello World!
... codice da eseguire dopo di funzione_parametro ...

Per quanto questa forma sia molto utile per spiegare il modo in cui un decoratore è strutturato, la sintassi tipica con cui vengono utilizzati I decoratori in Python è invece la seguente:

@funzione_decoratore
def mia_funzione(): 
    print("Hello World!") 

mia_funzione()
# output:

... codice da eseguire prima di funzione_parametro ...
hello world!
... codice da eseguire dopo di funzione_parametro ...

Quindi ciò che stiamo facendo coi decoratori, è sostanzialmente sostituire una funzione con un’altra. Questo fa si che alcuni dettagli riguardo a “mia_funzione” vadano persi. Vediamo di fare alcuni esempi. Facciamo un primo test senza utilizzare il decoratore:

def mia_funzione(): 
    print("Hello World!") 
    
>>> print(mia_funzione.__name__)
mia_funzione

Otteniamo in output “mia_funzione”, il nome della nostra funzione. Ma facciamo ora un secondo test, decorando la funzione con @funzione_decoratore:

@funzione_decoratore
def mia_funzione(): 
    print("Hello World!") 

>>> print(mia_funzione.__name__)
wrapper

Ecco che così facendo abbiamo perso delle informazioni preziose ad esempio, in fase di debugging. Per ovviare al problema e salvaguardare queste informazioni, chiamate come in altri contesti metadata, possiamo utilizzare un decoratore apposito incluso con la standard library, il decoratore wraps, in questo modo:

from functools import wraps
    
def funzione_decoratore(funzione_parametro): 
    @wraps(funzione_parametro)
    def wrapper(): 
        """ nome convenzionale - wrapper significa 'incarto, confezione' """
        print("... codice da eseguire prima di 'funzione_parametro' ...") 					
        funzione_parametro()
        print("... codice da eseguire dopo di 'funzione_parametro' ...") 
    return wrapper 

@funzione_decoratore
def mia_funzione(): 
    print("Hello World!") 

>>> print(mia_funzione.__name__)
mia_funzione

Come abbiamo detto all’inizio, oltre ad estendere possiamo anche modificare il comportamento delle nostre funzioni.

Facciamo ora un esempio in cui vogliamo un decoratore @caps_lock, ovvero un decoratore che trasformi l’output delle nostre funzioni in output a lettere maiuscole:

def caps_lock(funzione_parametro):
    @wraps(funzione_parametro)
    def wrapper():
        """ wrapper significa 'incarto, confezione' """
        return funzione_parametro().upper()
    return wrapper
    
    
@caps_lock
def mia_funzione():
    return "hello world!"
    

>>> print(mia_funzione()) 
HELLO WORLD!

Vediamo ora come sia possibile decorare anche funzioni che accettano parametri, mediante l’utilizzo di *args e **kwargs.

Per chi non avesse familiarità coi due, si tratta semplicemente di un modo che ci consente di passare un numero indefinito di parametri posizionali args, e parametri chiave-valore, quindi Key-Word, kwargs.

Teniamo inoltre a mente che, una funzione annidata all’interno di un’altra funzione, può ricordare lo Scope, ovvero l’Ambito, della funzione in cui è definita. Ci riferiamo a queste come Closures.

def caps_lock(funzione_parametro):
    @wraps(funzione_parametro)
    def wrapper(*args, **kwargs):
        """ wrapper significa 'incarto, confezione' """
        return funzione_parametro(*args, **kwargs).upper()
    return wrapper

    
@caps_lock
def echo(msg):
    return msg


>>> print(echo("I decoratori sono fantastici, e molto potenti!"))
I DECORATORI SONO FANTASTICI, E MOLTO POTENTI!

Un’altra cosa da sapere riguardo ai decoratori, è che possiamo concatenare anche più di uno nella stessa funzione. Aggiungiamo un altro decoratore, @spam:

def spam(funzione_parametro):
    @wraps(funzione_parametro)
    def wrapper(*args, **kwargs):
        print("SPAM!")
        return funzione_parametro(*args, **kwargs)
    return wrapper
Ora per concatenarli ci basta fare:
@spam
@caps_lock
def echo(msg):
    return msg


>>> print(echo("eggs & bacon!"))
SPAM!
EGGS & BACON!

Infine, seppur forse non si tratta della tecnica più usata, è possibile creare decoratori anche a partire da classi.

Ad esempio, questa è una possibile versione del decoratore @caps_lock, fatta con una classe:

class Caps_lock:
    
    def __init__(self, funzione_parametro):
        self.funzione_parametro = funzione_parametro

    def __call__(self, *args, **kwargs):
        return self.funzione_parametro(*args, **kwargs).upper()
Menu della Serie