Archivi tag: python

Python decorator

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 closurejavascript-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().