PDA

View Full Version : [Guida] Python e decoratori


Ryuzaki_Eru
18-07-2010, 11:48
Ho scritto questa "guida" sui decoratori in Python dato che molti li considerano ostici e poco chiari e in italiano non c'era praticamente nulla.
Non è di certo il massimo della chiarezza e il mio talento da scrittore evidentemente non esiste o se esiste è molto nascosto :D Ma spero che possa essere utile a qualcuno.

Buona lettura!

--------------------------------------------------------------------------

I decoratori, spesso, mettono in difficoltà o fanno paura anche ai programmatori più navigati. Sarà il loro nome, sarà che a prima vista sembrano arabo, ma fattostà che è cosi.

Ma cerchiamo di capire un pò meglio cosa sono questi fantomatici "decoratori".

Cosa è e cosa fa un decoratore?

Un decoratore è una normale funzione che modifica un'altra funzione. Quando si usa un decoratore, Python passa la funzione da decorare al decoratore, e la sostituisce con il risultato. Facciamo un esempio senza usare la sintassi tipica dei decoratori:

def mio_decoratore(funzione_da_decorare):
#fai qualcosa con la funzione, per es.
funzione_da_decorare.prova = "prova"
return funzione_da_decorare

def molt(a, b):
return a * b

molt = mio_decoratore(molt)

Cosa abbiamo fatto? Per prima cosa abbiamo definito una funzione "molt"(che rappresenta la nostra funzione da decorare) che effettua la moltiplicazione tra due numeri e successivamente l'abbiamo sostituita con la funzione restituita da "mio_decoratore", che altro non è che la funzione "molt" modificata. Infatti, se proviamo a digitare:

molt.prova

otteniamo:

'prova'

Adesso scriviamo un altro codice che fa la stessa identica cosa, ma che usa la sintassi tipica dei decoratori:

def mio_decoratore(funzione_da_decorare):
funzione_da_decorare.prova = "prova"
return funzione_da_decorare

@mio_decoratore
def molt(a, b):
return a * b

La sintassi è molto semplice: basta mettere sopra la funzione da decorare il simbolo @ seguito dal decoratore, in questo caso "mio_decoratore"(può avere un nome qualunque, ovviamente).

Internamente Python applica la funzione decorata al decoratore e la sostituisce con il valore restituito da quest'ultimo. Proprio come nel primo esempio. Semplice no?

Domanda: ma un decoratore deve restituire per forza una funzione? No. Può restituire tutto quello che volete, ma la sua utitlità diventa praticamente nulla :) Immaginate un caso come questo:

def decoratore_inutile(funzione_da_decorare):
return True

@decoratore_inutile
def molt(a, b):
return a * b

Adesso "molt" non è più una funzione, ma un valore booleano, cioè True. Se provate a chiamarla, otterrete il fatidico "TypeError":

>>>molt(5, 5)
>>>TypeError: 'bool' object is not callable

Come vedete non è che serva a molto :)

Un decoratore può restituire una funzione arbitraria, ricordate? Questa funzione la chiameremo "wrapper" e sarà subito chiaro il perchè.
Il giochetto sta nel definire la funzione wrapper all'interno del decoratore, in modo che possa utilizzare le variabili presenti nel suo name space, inclusa quindi anche la funzione da decorare che verrà passata al decoratore.

def mio_decoratore(funzione_da_decorare):
def wrapper():
print "Sono dentro la funzione wrapper e posso accedere " \
"alla funzione %s" % funzione.__name__
return funzione_da_decorare()

return wrapper

@mio_decoratore
def foo():
print "Io sono la funzione da decorare"

>>>foo()
Sono dentro la funzione wrapper e posso accedere alla funzione foo
Io sono la funzione da decorare

Come vedete la funzione wrapper può fare quello che vuole alla funzione da decorare. Ma qualcuno a questo punto si potrà chiedere: e se devo passare degli argomenti alla funzione da decorare?

Semplice: li passiamo alla funzione wrapper. Questo perchè? Perchè quando usiamo il decoratore, la funzione wrapper sostituirà la funzione da decorare. Quindi sarà lei a ricevere gli argomenti in modo da poterli usare per il proprio scopo.

Dal momento che un decoratore può lavorare con qualunque funzione, non sappiamo quanti argomenti avrà la funzione da decorare, quindi il problema si risolve semplicemente facendo accettare alla funzione wrapper argomenti arbitrari non posizionali e a parola chiave, che poi passerà alla funzione da decorare. Ma facciamo un esempio chiarificatore:

def mio_decoratore(funzione_da_decorare):
def wrapper(*args, **kwargs):
print "Stiamo chiamando la funzione %s con argomenti " \
"%s e parole chiave %s" % (funzione_da_decorare.__name__, args, kwargs)
return funzione_da_decorare(*args, **kwargs)

return wrapper

@mio_decoratore
def molt(a, b, foo = "foo"):
print foo
return a * b

>>>molt(1, 2)
Stiamo chiamando la funzione molt con argomenti (1,2) e parole chiave {}
foo
2

Con gli argomenti possiamo fare ciò che vogliamo, ad esempio:

def mio_decoratore(funzione_da_decorare):
def wrapper(*args, **kwargs):
kwargs['foo'] = "argomento modificato"
print "Stiamo chiamando la funzione %s con argomenti " \
"%s e parole chiave %s" % (funzione_da_decorare.__name__, args, kwargs)
return funzione_da_decorare(*args, **kwargs)

return wrapper

@mio_decoratore
def molt(a, b, foo = "foo"):
print foo
return a * b

>>>molt(1, 2)
Stiamo chiamando la funzione molt con argomenti (1,2) e parole chiave {'foo': 'argomento modificato'}
argomento modificato
2

Potresti aver bisogno di personalizzare il comportamento del tuo decoratore passandogli delle opzioni.
Per fare questo dobbiamo definire la nostra funzione decoratore dentro un'altra funzione, che chiameremo "opzioni". Nel momento in cui vogliamo decorare una funzione si usa la solita sintassi(@nome_decoratore), ma invece di usare il nostro decoratore useremo la funzione "opzioni". Vediamo subito un esempio:

def opzioni(valore):
def mio_decoratore(funzione_da_decorare):
funzione_da_decorare.prova = valore
return funzione_da_decorare
return mio_decoratore

@opzioni('test')
def molt(a, b):
return a * b

>>>molt(5, 5)
25
>>>molt.prova
'test'

Non cambia poi molto no? Solo che adesso il nostro decoratore si trova in un ambito dinamico, invece di uno statico :)

E se insieme alle opzioni vogliamo usare anche una funzione wrapper? Il procedimento è esattamente lo stesso:

def opzioni(nome):
def mio_decoratore(funzione_da_decorare):
def wrapper(*args, **kwargs):
kwargs.update({'nome': nome})
print "Chiamo la funzione %s" % funzione_da_decorare
return funzione_da_decorare(*args, **kwargs)
return wrapper
return mio_decoratore

@opzioni("pinco")
def stampa_nome(nome = None):
print "Ciao ", nome

Come avrete già capito, la funzione "opzioni" restituisce il decoratore, che Python userà normalmente; cioè, passerà la funzione da decorare al decoratore e verrà restituito il risultato. Infatti:

>>>stampa_nome
>>><function wrapper at 0x00BBA930>

vedete che "stampa_nome" non è la funzione decoratore(come invece si potrebbe pensare), ma bensì la funzione wrapper. E' quindi evidente che la nostra funzione da decorare("stampa_nome") è stata passata implicitamente per noi da Python ed è stata sostituita con il valore restituito dal decoratore. Bello no? :)

Questo è più o meno tutto.

Esempi d'uso, i decoratori @staticmethod, @classmethod e @property

Riferimenti:

modulo "decorator" scritto da Michele Simionato (http://micheles.googlecode.com/hg/decorator/documentation.html)

--------------------------------------------------------------------------

Il contenuto di questo post è rilasciato con licenza Creative Commons Attribution-Noncommercial-Share Alike 2.5 (http://creativecommons.org/licenses/by-nc-sa/2.5/it/)

das
18-07-2010, 13:29
Grazie e complimenti per la tua guida, è la prima volta che sento parlare di decoratori (avrò usato python 3 volte), solo che non mi immagino un caso pratico in cui serva usarli e semplifichino la vita.
Potresti fare un esempio?

Ciao

Ryuzaki_Eru
18-07-2010, 14:04
Caso pratico comune: @property, @staticmethod e @classmethod. Adesso sono incasinato, tu documentati. Quando posso casomai scrivo qualche esempio.
In generale, quando hai bisogno di modificare o fare operazioni su una funzione, allora i decoratori fanno al caso tuo. Ovviamente non se ne deve abusare e quello che fai con i decoratori lo puoi fare anche senza, però semplificano la vita in alcuni casi.
Esempio: mettiamo che hai scritto un'applicazione in Django e vuoi fare in modo che possa essere usata solo dall'admin. Puoi benissimo scriverti un decoratore che controlla se la vista richiesta sia stata "chiamata" dall'admin o no e quindi determinare le operazioni da seguire o sollevare un'eccezione.

cdimauro
19-07-2010, 08:38
Un caso pratico, reale, e comunissimo è il log.

Mettiamo che debba loggare la chiamata a una funzione, i suoi parametri, e il valore di ritorno.

Scrivo un decoratore log che restituisce un wrapper che "ingloba" la funzione da chiamare. A questo punto quando il wrapper viene invocato, logga il nome della funzione che ha inglobato, e i parametri che gli sono stati passati. A questo punto richiama la funzione e conserva il risultato. Logga il risultato. E infine restituisce il valore al chiamante.

Per qualunque funzione (o metodo) da loggare basta scrivere:
@log
def MiaFunzione(): pass
Semplice e pulito.

Ancora più interessanti sono i decoratori di classi. Si può scrivere un decoratore di classe (che è una funzione), che invece di ricevere in input una funzione, riceve una classe. A questo punto possiamo prendere l'elenco di tutti i suoi metodi e applicarci una decoratore di log, in modo che tutti i suoi metodi siano loggati.

Quindi per loggare tutti i metodi di una classe basta scrivere:
@log
class MiaClasse():

def f(self): pass

def g(self): pass
Supposto che log sia la funzione di decorazione della classe, questa restituirà sempre la classe MiaClasse, ma con tutti i metodi di quest'ultima che saranno loggati.

Comunque complimenti per la guida: chiara e semplice. Se aggiungi i decoratori di classe (disponibili da Python 2.6) secondo me potrebbe essere messa nella sezione dei tutorial. ;)

Torav
19-07-2010, 10:28
Ottima guida, grazie mille! :)

Ryuzaki_Eru
19-07-2010, 15:26
Comunque complimenti per la guida: chiara e semplice. Se aggiungi i decoratori di classe (disponibili da Python 2.6) secondo me potrebbe essere messa nella sezione dei tutorial. ;)

La guida l'avevo scritta per il mio blog, poi ho chiesto a cionci che mi ha detto che andava bene per la sezione tutorial e cosi l'ho "modificata".
Comunque è un argomento interessante questo dei decoratori di classe. Appena posso l'aggiungo :)
Se vuoi puoi anche aggiungere tu degli esempi alla tua spiegazione che hai appena scritto e il 90% è già praticamente fatto :D

DanieleC88
19-07-2010, 17:27
Caso pratico comune: @property, @staticmethod e @classmethod.

Ecco, bravo, fai qualche esempio con le classi "nuovo stile", discendenti di object. :D

ciao, e complimenti per il lavoro ;)

Ryuzaki_Eru
19-07-2010, 18:53
Ecco, bravo, fai qualche esempio con le classi "nuovo stile", discendenti di object. :D

ciao, e complimenti per il lavoro ;)

Aggiungerò un esempio anche con questi ;)

cdimauro
19-07-2010, 22:35
La guida l'avevo scritta per il mio blog, poi ho chiesto a cionci che mi ha detto che andava bene per la sezione tutorial e cosi l'ho "modificata".
Comunque è un argomento interessante questo dei decoratori di classe. Appena posso l'aggiungo :)
Se vuoi puoi anche aggiungere tu degli esempi alla tua spiegazione che hai appena scritto e il 90% è già praticamente fatto :D
Eccolo qui l'esempio:
from functools import wraps

def log(Function):
'Decorator to log a function call, its parameters, and its result'

@wraps(Function)
def Wrapper(*Args, **Keywords):

print Function.__name__, Args, Keywords

Result = Function(*Args, **Keywords)

print Function.__name__, '->', Result

return Result

return Wrapper


@log
def f(x, y, z):
return x * y * z

f(1, 2, 3)


@log
def g(*Args, **Keywords):
return 'Argomenti: %s. Keyword: %s.' % (Args, Keywords)

g(1, 2, 3, y = 'a', z = 'b', x = 'c')
E questo è il risultato:
f (1, 2, 3) {}
f -> 6
g (1, 2, 3) {'y': 'a', 'x': 'c', 'z': 'b'}
g -> Argomenti: (1, 2, 3). Keyword: {'y': 'a', 'x': 'c', 'z': 'b'}.
Questo decoratore stampa a video, ma basta modificarlo per poter scrivere tutte le informazioni su file, db, ecc., senza andare a toccare le funzioni che "decora".

Ne uso altri anche per validare l'input, oppure per intercettare eventuali eccezioni.

Coi decoratori c'è ampio spazio per l'immaginazione. :)

Ryuzaki_Eru
20-07-2010, 12:00
Bene, poi lo aggiungo alla guida con i riferimenti a te. Appena posso scrivo degli esempi con @property, @staticmethod e @classmethod, e un esempio di decoratore di classe.

cdimauro
21-07-2010, 10:18
Lascia perdere i riferimenti a me. ;)

Ryuzaki_Eru
21-07-2010, 10:19
L'hai scritto tu quel pezzo, è giusto che metto i riferimenti :)

nardellu
21-07-2010, 21:49
ottima spiegazione complimenti!;)

la prima volta che mi sono imbatutto su un decorator è stato su django... @login_required :mbe: :mbe: :mbe:

fortuna che ho intuito al volo la loro funzione!;)

Ryuzaki_Eru
01-08-2010, 10:38
Al più presto aggiorno la guida, scriverò degli esempi sui decoratori predefiniti in Python e vedrò di fare smanettare sui decoratori di classe che sono particolari, anche se il funzionamento e il concetto è identico a quelli delle funzioni.

Ryuzaki_Eru
19-05-2011, 09:14
Noto che la guida non è stata ancora spostata nell'apposita sezione...come mai?

cdimauro
19-05-2011, 13:58
Forse cionci aspetta che completi l'argomento coi decoratori di classe.

Ryuzaki_Eru
19-05-2011, 14:05
Forse cionci aspetta che completi l'argomento coi decoratori di classe.

Al momento la vedo difficile...se qualcuno lo vuole completare(se il problema è questo) per me va bene. Altrimenti c'è da aspettare un pò.

cdimauro
19-05-2011, 14:18
Allora aspetteremo. Comunque per un'introduzione ai decoratori quanto già scritto va bene.

Ryuzaki_Eru
20-05-2011, 09:20
Allora aspetteremo. Comunque per un'introduzione ai decoratori quanto già scritto va bene.

Anche perchè dopo tutto questo tempo fermo devo fare una bella rispolverata di tutto.