Home > python > Python: decoratori per programmatori di coccio

Python: decoratori per programmatori di coccio

13 Giugno 2013

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

Categorie:python Tag:
I commenti sono chiusi.