Python è un linguaggio che deve gran parte del suo successo alla chiarezza ed alla semplicità. Eppure, come spesso accade, sono i suoi lati oscuri ad affascinare i programmatori.
Questo è il caso dei decorator. Si trovano sulla rete un sacco di articoli che ne parlano un gran bene decantandone l’utilità e la semplicità. Non metto in discussione la loro utilità, ma nutro qualche dubbio sulla loro chiarezza. Per i miei gusti il loro comportamento è un po’ troppo “magico”. Quando incontro qualche costrutto il cui funzionamento va in qualche modo interpretato nel contesto mi metto in allarme.
A suo tempo Java fu considerato un progresso rispetto al C nella semplicità per la rinuncia alla gestione esplicita dei puntatori.
Ma mi è capitato varie volte di vedere dei programmatori Java (e non solo) alle prime armi combinare dei pasticci perché non si rendono conto che l’array o l’object che modificano dentro un metodo, è, al netto del nome, proprio lo stesso che è passato come parametro. Nel vecchio Pascal si doveva esplicitare se un parametro era passato come valore o come variabile, forse era una pedanteria e forse i puntatori del C sono scomodi, ma i comportamenti nascosti sono pericolosi.
Una descrizione molto informale dei decorator potrebbe essere:
Un decoratore è una funzione che avvolge un’altra funzione e ne può manipolare i parametri ed il risultato.
Volendo schematizzare per una funzione func.
func_decorator(func) qui si può agire sui parametri (args) ret=func(args) qui si può agire sul risultato (ret) ritorna ret modificato
È evidente che con una simile descrizione sia ha una comprensione intuitiva del funzionamento dei decorator, probabilmente sufficiente ad utilizzarli quando si trovano in qualche libreria, ma si ignora il reale meccanismo del passaggio della funzione e degli argomenti. Per sfruttarli al meglio i decorator ed evitare di cadere in abusi è opportuna una conoscenza più approfondita. L’unico modo, come sempre, è quello di implementare qualche esempio.
Per focalizzare tutta l’attenzione sulla costruzione dei decoratori ho scelto per gli esempi delle funzioni banali. Questo, in prima istanza, non aiuterà a comprendere l’utilità dei decorator ma semplifica la comprensione del loro funzionamento. Solo se si sa implementare senza sforzo un decorator si è in grado di capire fino in fondo la loro utilità e di utilizzare correttamente quelli che si incontrano nelle api delle librerie e nei framework.
es.1
def add_decorate(x, y, a, b): print('args : %s %s' % (x, y)) xb = x * a yb = y * a print('args update : %s %s' % (xb, yb)) r = add(xb, yb) print('response :%s' % (r)) ra = r / b print('response update :%s' % (ra)) return ra r = add(40, 10) print('add(40,10) => %s' % (r)) r = add_decorate(40, 10, 10, 2) print('add_decorate(40,10,10,2) => %s' % (r))
Output:
add(40,10) => 50 args : 40 10 args update : 400 100 response :500 response update :250 add_decorate(40,10,10,2) => 250
Nell’esempio es.1 tutto è molto semplice e il decorator di fatto non è che un’organizzazione particolare del codice. Si è implementata una funzione add_decorate() che utilizza al suo interno la funzione add().
Ora cerchiamo di generalizzare.
es.2
def add(x, y): return x + y def sub(x, y): return x - y def mult(x, y): return x * y def div(x, y): return x / y def op_decorate(func): def wrapper(x, y): a = 10 b = 2 x_dec = x * a y_dec = y * a response = func(x_dec, y_dec) response_dec = response / b return response_dec return wrapper r = add(40, 10) print('add (40,10)=> %s' % (r)) r = op_decorate(add)(40, 10) print('op_decorate(add)(40,10) => %s' % (r)) add_dec = op_decorate(add) r = add_dec(40, 10) print('add_dec(40,10) => %s' % (r)) print('----') r = sub(40, 10) print('sub (40,10)=> %s' % (r)) r = op_decorate(sub)(40, 10) print('op_decorate(sub)(40,10) => %s' % (r)) sub_dec = op_decorate(sub) r = sub_dec(40, 10) print('sub_dec(40,10) => %s' % (r)) print('----') r = mult(40, 10) print('mult (40,10)=> %s' % (r)) r = op_decorate(mult)(40, 10) print('op_decorate(mult)(40,10) => %s' % (r)) mult_dec = op_decorate(mult) r = mult_dec(40, 10) print('mult_dec(40,10) => %s' % (r)) print('----') r = div(40, 10) print('div (40,10)=> %s' % (r)) r = op_decorate(div)(40, 10) print('op_decorate(div)(40,10) => %s' % (r)) div_dec = op_decorate(div) r = div_dec(40, 10) print('div_dec(40,10) => %s' % (r))
Output:
add (40,10)=> 50 op_decorate(add)(40,10) => 250 add_dec(40,10) => 250 ---- sub (40,10)=> 30 op_decorate(sub)(40,10) => 150 sub_dec(40,10) => 150 ---- mult (40,10)=> 400 op_decorate(mult)(40,10) => 20000 mult_dec(40,10) => 20000 ---- div (40,10)=> 4 op_decorate(div)(40,10) => 2 div_dec(40,10) => 2
L’esempio es.2 è utilizzabile con funzioni diverse.
Notate che per funzionare deve utilizzare la closure ( javascript-closures ) per la funzione func.
La funzione func appartiene al Context individuato da op_decorate().
La funzione op_decorate() ritorna wrapper(), ma func definita al suo esterno “continua a vivere” perché appartiene al suo Context e quindi wrapper() la può utilizzare.
es.3
def add(x, y): return x + y def sub(x, y): return x - y def mult(x, y): return x * y def div(x, y): return x / y def op_decorate_ab(a, b): def op_decorate(func): def wrapper(x, y): x_dec = x * a y_dec = y * a response = func(x_dec, y_dec) response_dec = response / b return response_dec return wrapper return op_decorate r = add(40, 10) print('add(40,10) => %s' % (r)) """ op_decorate_ab(a,b) ritorna la funzione op_decorate(func) a,b esistono ne Context op_decorate(func) ritorna la funzione wrapps(x,y) func esiste nel context wrap(x,y) utilizza le variabili del Context a,b,func """ r = op_decorate_ab(10, 2)(add)(40, 10) print('op_decorate_ab(10,2)(add)(40,10) => %s' % (r)) """ uso esplicito della funzione decorata add_dec(40,10) """ add_dec = op_decorate_ab(10, 2)(add) r = add_dec(40, 10) print('add_dec(40,10) => %s' % (r)) print('----') r = sub(40, 10) print('sub(40,10) => %s' % (r)) r = op_decorate_ab(10, 2)(sub)(40, 10) print('op_decorate_ab(10,2)(sub)(40,10) => %s' % (r)) sub_dec = op_decorate_ab(10, 2)(sub) r = sub_dec(40, 10) print('sub_dec(40,10) => %s' % (r)) print('----') r = add(40, 10) print('mult(40,10) => %s' % (r)) r = op_decorate_ab(10, 2)(mult)(40, 10) print('op_decorate_ab(10,2)(mult)(40,10) => %s' % (r)) mult_dec = op_decorate_ab(10, 2)(mult) r = mult_dec(40, 10) print('mult_dec(40,10) => %s' % (r)) print('----') r = div(40, 10) print('div(40,10) => %s' % (r)) r = op_decorate_ab(10, 2)(div)(40, 10) print('op_decorate_ab(10,2)(div)(40,10) => %s' % (r)) div_dec = op_decorate_ab(10, 2)(div) r = div_dec(40, 10) print('div_dec(40,10) => %s' % (r))
Output:
add(40,10) => 50 op_decorate_ab(10,2)(add)(40,10) => 250 add_dec(40,10) => 250 ---- sub(40,10) => 30 op_decorate_ab(10,2)(sub)(40,10) => 150 sub_dec(40,10) => 150 ---- mult(40,10) => 50 op_decorate_ab(10,2)(mult)(40,10) => 20000 mult_dec(40,10) => 20000 ---- div(40,10) => 4 op_decorate_ab(10,2)(div)(40,10) => 2 div_dec(40,10) => 2
Nell’esempio es.3 si implementa un decorator con parametri per aumentarne la flessibilità.
Anche in questo caso si sfruttano le proprietà delle variabili di Context. Prima per i parametri x,y della funzione op_decorate_ab(x,y), poi per il parametro func di op_decorate(func).
es.4
def op_decorator(a, b): def op_decorate(func): def wrapper(x, y): x_dec = x * a y_dec = y * a response = func(x_dec, y_dec) response_dec = response / b return response_dec return wrapper return op_decorate @op_decorator(10, 2) def add(x, y): return x + y @op_decorator(10, 2) def sub(x, y): return x - y @op_decorator(10, 2) def mult(x, y): return x * y @op_decorator(10, 2) def div(x, y): return x / y r = add(40, 10) print('add(40,10) => %s' % (r)) print('add => %s ' % (add)) print('----') r = sub(40, 10) print('sub(40,10) => %s' % (r)) print('sub => %s ' % (sub)) print('----') r = mult(40, 10) print('mult(40,10) => %s' % (r)) print('mult => %s ' % (mult)) print('----') r = div(40, 10) print('div(40,10) => %s' % (r)) print('div => %s ' % (div))
Output:
add(40,10) => 250 add => function wrapper at 0x7f3f29a48848 ---- sub(40,10) => 150 sub => function wrapper at 0x7f3f29a48938 ---- mult(40,10) => 20000 mult => function wrapper at 0x7f3f29a48a28 ---- div(40,10) => 2 div => function wrapper at 0x7f3f29a48b18
Finalmente nell’esempio es.4 fa la sua comparsa la notazione specifica di decorator.
Rispetto all’esempio es.3 è cambiato solo il modo di invocare la funzione.
Con la nuova notazione si modifica il comportamento della funzione decorata senza cambiarne il nome e senza modificare la funzione stessa.
Nell’esempio è stata aggiunta la stampa della funzione per evidenziare che viene sempre restituita la funzione wrapper(), questo può causare qualche problema, specialmente nel debug.
es.5
import functools def op_decorator(a, b): def op_decorate(func): @functools.wraps(func) def wrapper(x, y): x_dec = x * a y_dec = y * a response = func(x_dec, y_dec) response_dec = response / b return response_dec return wrapper return op_decorate
Output:
add(40,10) => 250 add => function add at 0x7f371cd5da28 ---- sub(40,10) => 150 sub => function sub at 0x7f371cd5db18 ---- mult(40,10) => 20000 mult => function mult at 0x7f371cd5dc08 ---- div(40,10) => 2 div => function div at 0x7f371cd5dcf8
Nell’esempio es.5 si risolve il problema con l’aggiunta di una semplice riga fornita dalla libreria functools. Con la modifica viene restituita la funzione decorata con il suo nome originale.
Basta decorare la funzione wrapper() con @functools.wraps(func).
Un decorator viene in aiuto per risolvere un problema nella costruzione del decorator stesso.
Un esercizio interessante sarebbe quello di capire cosa fa @functools.wraps().