Python: decoratori per programmatori di coccio
L’argomento è inizialmente indigesto per chi, come me, non è un programmatore esperto.
Cerchiamo di semplificare il tutto, con parole semplici ed esempi ‘digeribili’.
# Cos’è un decoratore?
Cominciamo con 2 definizioni fondamentali per arrivare a capire il decoratore:
1 – “Un decoratore è una funzione che modifica un’altra funzione”.
2 – “In Python anche le funzioni sono oggetti!”
Partiamo dalla definizione 1.
1a- Un decoratore è una funzione che modifica un’altra funzione.
1b- Il decoratore si aspetta un’altra funzione come argomento (funzione che modificherà).
1c- Il decoratore, una volta modificata, ritornerà la funzione passatagli come argomento.
La seconda definizione, non dovrebbe lasciarci più di tanto perplessi, dal momento che python,
vive di oggetti.
Il primo esempio vale più di mille parole:
def funzione_decoratore(funzione_da_decorare): # facciamo qualsiasi operazione sulla funzione # esempio assegnarle un attributo, ricordate? # in python le funzioni sono oggetti. funzione_da_decorare.attributo = "decorata!" print 'func: {} : -> {}'.format(funzione_da_decorare.__name__, funzione_da_decorare.attributo) return funzione_da_decorare
Attenzione al return!
Stiamo ritornando la funzione, non il risultato della funzione stessa, che avremmo ottenuto
chimandola con l’uso delle parentesi ()!!
ora prendiamo una stupidissima funzione:
def saluta(): return "Hello world"
passiamola al decoratore che la sostituirà dopo averla modificata:
>>> saluta = funzione_decoratore(saluta) func: saluta : -> decorata! >>> saluta <function saluta at 0x011D45F0> # abbiamo ritornato la funzione, non l'abbiamo eseguita >>> saluta.attributo 'decorata!' >>> saluta() 'Hello world' # qui abbiamo eseguito la funzione!
come si nota il comportamento della funzione saluta, non è cambiato, in compenso ora,
la funzione ha un attributo che le è stato assegnato dal decoratore (oltre ai print
di “debug” che abbiamo messo per capire quale sia l’ordine dei codici eseguiti).
Ovviamente il decoratore può ritornare la funzione su un’altra variabile, ma la magia sta
proprio nel ritornare la funzione, sulla funzione stessa (in modo appunto da ‘decorarla’)
La sintassi precedente è sostituibile con qualcosa di più pythonico, grazie all’utilizzo
del carattere ‘@’:
>>> @funzione_decoratore def saluta(): return "Hello world" func: saluta : -> decorata!
In pratica, appena prima della funzione da usare, si utilizza il carattere ‘@’
seguito dal nome della funzione che fa da decoratore.
# Un decoratore con all’interno una funzione di wrapper
Il wrapper (o qualsiasi altro nome si utilizzi), è una funzione definita all’interno della
funzione decoratore. Questo wrapper ‘avvolge’ la funzione passata come argomento al decoratore,
così da poter eseguire codice, prima e dopo di essa.
Wrapper letteralmente significa ‘involucro’:
def funzione_decoratore_con_wrapper(funzione_da_decorare): def wrapper(): # possiamo fare cose su funzione_da_decorare # eseguo codice prima della funzione da decorare print "Sono nella funzione wrapper" print "avvolgo la funzione {}".format(funzione_da_decorare.__name__) # chiamo la funzione funzione_da_decorare() # eseguo codice dopo la funzione da decorare print "eseguo codice dopo aver chiamato la funzione, sono ancora in wrapper" return wrapper
@funzione_decoratore_con_wrapper def foo(): print "sono in foo e faccio cose..."
chiamando la funzione l’output sarà:
>>> foo() Sono nella funzione wrapper avvolgo la funzione foo sono in foo e faccio cose... eseguo codice dopo aver chiamato la funzione, sono ancora in wrapper
Una volta dentro a wrapper, possiamo fare qualsiasi operazione sulla funzione da decorare.
# E se la funzione da decorare avesse bisogno di argomenti?
Dovremmo passare gli argomenti alla funzione di wrapper interna.
La sintassi diventerebbe la seguente:
def funzione_decoratore_con_argomenti(funzione_da_decorare): def wrapper_con_argomenti(*args, **kwargs): print "Sono nella funzione wrapper" print "lavoro con {}".format(funzione_da_decorare.__name__) print "con argomenti {} e chiavi {}".format(args, kwargs) funzione_da_decorare(*args, **kwargs) return wrapper_con_argomenti
il solito esempio:
@funzione_decoratore_con_argomenti def saluta(nome, cognome): print "ciao {} {}".format(nome, cognome)
l’output sarà:
>>> saluta('Tizio', 'Caio') Sono nella funzione wrapper lavoro con saluta con argomenti ('Tizio', 'Caio') e chiavi {} ciao Tizio Caio
# Modifica degli argomenti passati
Ovviamente gli argomenti della funzione da decorare, all’interno di wrapper, possono
essere modificati:
def funzione_decoratore_con_argomenti(funzione_da_decorare): def wrapper_con_argomenti(*args, **kwargs): print "Sono nella funzione wrapper" print "lavoro con {}".format(funzione_da_decorare.__name__) print "con argomenti {} e chiavi {}".format(args, kwargs) print "modifico il cognome" kwargs['cognome'] = 'sempronio' funzione_da_decorare(*args, **kwargs) return wrapper_con_argomenti
@funzione_decoratore_con_argomenti def saluta(nome=None, cognome=None): print "ciao {} {}".format(nome, cognome)
l’output sarà:
>>> saluta('Tizio', cognome='Caio') Sono nella funzione wrapper lavoro con saluta con argomenti ('Tizio',) e chiavi {'cognome': 'Caio'} modifico il cognome ciao Tizio sempronio
# Come si passano argomenti al decoratore?
Se volessimo passare dei valori come argomenti al decoratore, la sintassi
sarebbe la seguente:
esempio senza wrapper:
def decoratore_con_argomenti(stato): def funzione_decoratore(funzione_da_decorare): funzione_da_decorare.stato = stato return funzione_da_decorare return funzione_decoratore
esempio:
@decoratore_con_argomenti('online') def login(user): print 'benvenuto {}'.format(user)
>>> login('bancaldo') benvenuto bancaldo >>> login.stato 'online'
lo stesso tipo di esempio, con però una funzione di wrapper all’interno del decoratore:
def opzioni_decoratore(stato): def funzione_decoratore(funzione_da_decorare): def wrapper_con_argomenti(*args, **kwargs): kwargs.update({'stato': stato}) funzione_da_decorare(*args, **kwargs) return wrapper_con_argomenti return funzione_decoratore
esempio:
@opzioni_decoratore('online') def login(user, stato='offline'): print 'user {}: {}'.format(user, stato)
output:
>>> login('bancaldo') user bancaldo: online
# Decoratori di metodi
In python, le funzioni e i metodi sono quasi la stessa cosa. L’unica differenza è che i secondi,
si aspettano come primo argomento, un riferimento all’oggetto corrente, che per convenzione
si indica con ‘self’.
Quindi se vogliamo utilizzare un decoratore per metodi, dobbiamo semplicemente ricordarci
il ‘self’.
ecco un esempio stupido (molto):
def decoratore_di_metodo(metodo_da_decorare): def wrapper_di_metodo(self, nome): print "il mio nome da civile è {}".format(nome) nome = 'james bond' return metodo_da_decorare(self, nome) return wrapper_di_metodo
creiamo una classe stupida per lavorare con un metodo:
class Spia: @decoratore_di_metodo def camuffa(self, nome): "camuffa il nome della spia" print "il mio nome da spia è {}".format(nome)
l’output è:
spia = Spia() spia.camuffa('bancaldo')
Nota: ringrazio Giornale di Sistema che mi ha fatto notare l’errore stupido delle due righe precedenti e i tab mal interpretati da wp.
# Dove e quando usare i decoratori nella realtà?
Beh un esempio pratico è quando abbiamo necessità di modificare una funzione
di una libreria esterna. Siccome non è buona cosa modificare le funzioni di librerie
esterne, si lavora su di esse con i decoratori.
Altro esempio, abbiamo una funzione che vogliamo si comporti diversamente in varie occasioni.
Per non venire meno al principio DRY (Don’t repeat yourself), creiamo solo una funzione e
tanti decoratori diversi a seconda delle esigenze.
Un esempio pratico l’ho trovato su questa meravigliosa risposta in S.O., dalla quale ho tratto
quasi tutto, traducendolo:
def benchmark(func): """ A decorator that print the time of function take to execute. """ import time def wrapper(*args, **kwargs): t = time.clock() res = func(*args, **kwargs) print func.__name__, time.clock()-t return res return wrapper def logging(func): """ A decorator that logs the activity of the script. (it actually just prints it, but it could be logging!) """ def wrapper(*args, **kwargs): res = func(*args, **kwargs) print func.__name__, args, kwargs return res return wrapper def counter(func): """ A decorator that counts and prints the number of times a function has been executed """ def wrapper(*args, **kwargs): wrapper.count = wrapper.count + 1 res = func(*args, **kwargs) print "{0} has been used: {1}x".format(func.__name__, wrapper.count) return res wrapper.count = 0 return wrapper @counter @benchmark @logging def reverse_string(string): return str(reversed(string)) print reverse_string("Able was I ere I saw Elba") print reverse_string("A man, a plan, a canoe, pasta, heros, rajahs, a coloratura, maps, snipe, percale, macaroni, a gag, a banana bag, a tan, a tag, a banana bag again (or a camel), a crepe, pins, Spam, a rut, a Rolo, cash, a jar, sore hats, a peon, a canal: Panama!") #outputs: #reverse_string ('Able was I ere I saw Elba',) {} #wrapper 0.0 #wrapper has been used: 1x #ablE was I ere I saw elbA #reverse_string ('A man, a plan, a canoe, pasta, heros, rajahs, a coloratura, maps, snipe, percale, macaroni, a gag, a banana bag, a tan, a tag, a banana bag again (or a camel), a crepe, pins, Spam, a rut, a Rolo, cash, a jar, sore hats, a peon, a canal: Panama!',) {} #wrapper 0.0 #wrapper has been used: 2x #!amanaP :lanac a ,noep a ,stah eros ,raj a ,hsac ,oloR a ,tur a ,mapS ,snip ,eperc a ,)lemac a ro( niaga gab ananab a ,gat a ,nat a ,gab ananab a ,gag a ,inoracam ,elacrep ,epins ,spam ,arutaroloc a ,shajar ,soreh ,atsap ,eonac a ,nalp a ,nam A
altri link:
decoratori in python
Commenti recenti