2015-05-07 19 views
13

considerare questo piccolo esempio:Come posso decorare un metodo di istanza con una classe decoratore?

import datetime as dt 

class Timed(object): 
    def __init__(self, f): 
     self.func = f 

    def __call__(self, *args, **kwargs): 
     start = dt.datetime.now() 
     ret = self.func(*args, **kwargs) 
     time = dt.datetime.now() - start 
     ret["time"] = time 
     return ret 

class Test(object): 
    def __init__(self): 
     super(Test, self).__init__() 

    @Timed 
    def decorated(self, *args, **kwargs): 
     print(self) 
     print(args) 
     print(kwargs) 
     return dict() 

    def call_deco(self): 
     self.decorated("Hello", world="World") 

if __name__ == "__main__": 
    t = Test() 
    ret = t.call_deco() 

che stampa

Hello 
() 
{'world': 'World'} 

Perché il parametro self (che dovrebbe essere il test obj esempio) non passata come primo argomento della funzione decorato decorated?

Se lo faccio manualmente, come:

def call_deco(self): 
    self.decorated(self, "Hello", world="World") 

funziona come previsto. Ma se devo sapere in anticipo se una funzione è decorata o no, sconfigge l'intero scopo dei decoratori. Qual è lo schema per andare qui o ho frainteso qualcosa?

+1

Un rapido google mostra questo: http://thecodeship.com/patterns/guide-to-python-function-decorators/ (vedere la sezione "Metodi di decorazione") – cfh

+1

Hai letto per es. http://stackoverflow.com/q/2365701/3001761, http://stackoverflow.com/q/15098424/3001761 – jonrsharpe

+1

Non ti imbatterai in questo tipo di problema quando utilizzi una funzione come decoratore anziché come callable oggetto. – poke

risposta

15

tl; dr

È possibile risolvere questo problema mediante la conversione della classe Timed un descrittore e il ritorno di una funzione parzialmente applicata da __get__ quale si applica l'oggetto Test come uno degli argomenti, come questo

class Timed(object): 

    def __init__(self, f): 
     self.func = f 

    def __call__(self, *args, **kwargs): 
     print self 
     start = dt.datetime.now() 
     ret = self.func(*args, **kwargs) 
     time = dt.datetime.now() - start 
     ret["time"] = time 
     return ret 

    def __get__(self, instance, owner): 
     from functools import partial 
     return partial(self.__call__, instance) 

Il vero problema

Citando la documentazione Python per decorator,

La sintassi decoratore è lo zucchero meramente sintattico, le due seguenti definizioni di funzione sono semanticamente equivalenti:

def f(...): 
    ... 
f = staticmethod(f) 

@staticmethod 
def f(...): 
    ... 

Così, quando tu dici,

@Timed 
def decorated(self, *args, **kwargs): 

realtà è

decorated = Timed(decorated) 

solo l'oggetto funzione è passato al Timed, l'oggetto a cui è vincolata in realtà non si trasmette con esso.Così, quando si richiama in questo modo

ret = self.func(*args, **kwargs) 

self.func farà riferimento alla oggetto funzione non legato e viene richiamata con Hello come primo argomento. Questo è il motivo per cui self viene stampato come Hello.


Come posso risolvere questo?

Dal momento che non si ha riferimento all'istanza Test nel Timed, l'unico modo per farlo sarebbe quello di convertire Timed come classe descrittore. Citando la documentazione, Invoking descriptors sezione

In generale, un descrittore è un attributo oggetto con “binding comportamento”, una cui accesso attributo sia stata ripristinata con i metodi del protocollo descrittore: __get__(), __set__() e __delete__(). Se uno di questi metodi è definito per un oggetto, si dice che sia un descrittore.

Il comportamento predefinito per l'accesso agli attributi è ottenere, impostare o eliminare l'attributo dal dizionario di un oggetto. Ad esempio, a.x ha una catena di ricerca che inizia con a.__dict__['x'], quindi type(a).__dict__['x'] e continua attraverso le classi base di type(a) esclusi i metaclassi.

Tuttavia, se il valore cercato è un oggetto che definisce uno dei metodi descrittori, Python può sovrascrivere il comportamento predefinito e richiamare il metodo descrittore invece.

Possiamo fare Timed un descrittore, definendo semplicemente un metodo come questo

def __get__(self, instance, owner): 
    ... 

Qui, self riferisce all'oggetto Timed stessa, instance riferisce all'oggetto effettiva in cui la ricerca attributo avviene e owner fa riferimento alla classe corrispondente allo instance.

Ora, quando __call__ viene richiamato su Timed, verrà invocato il metodo __get__. Ora, in qualche modo, è necessario passare il primo argomento come istanza della classe Test (anche prima dello Hello). Quindi, creiamo un'altra funzione parzialmente applicata, il cui primo parametro sarà l'istanza Test, simili

def __get__(self, instance, owner): 
    from functools import partial 
    return partial(self.__call__, instance) 

Ora, self.__call__ è un metodo vincolato (vincolati a Timed esempio) e il secondo parametro partial è il primo argomento alla chiamata self.__call__.

Quindi, tutti questi si traducono in modo efficace come questo

t.call_deco() 
self.decorated("Hello", world="World") 

Ora self.decorated è in realtà Timed(decorated) (questo sarà indicato come TimedObject d'ora in poi) oggetto.Ogni volta che accediamo, verrà invocato il metodo __get__ definito e verrà restituita una funzione partial. È possibile confermare che in questo modo

def call_deco(self): 
    print self.decorated 
    self.decorated("Hello", world="World") 

sarebbe stampare

<functools.partial object at 0x7fecbc59ad60> 
... 

Quindi,

self.decorated("Hello", world="World") 

si traduce per

Timed.__get__(TimedObject, <Test obj>, Test.__class__)("Hello", world="World") 

Essendo restituire una funzione di partial,

partial(TimedObject.__call__, <Test obj>)("Hello", world="World")) 

che è in realtà

TimedObject.__call__(<Test obj>, 'Hello', world="World") 

Quindi, <Test obj> diventa anche una parte di *args, e quando self.func viene invocato, il primo argomento sarà la <Test obj>.

+0

Perché usare un 'functool.partial' invece del built-in dedicato' types.MethodType'? –

+0

Grazie per questa risposta esauriente - Dovrò leggerlo un paio di volte per farlo, ma sono sicuro che lo farò! Quando ho provato per la prima volta ad applicare questo, ho lasciato fuori '(oggetto)' nella definizione del mio decoratore - quindi, ho appena avuto 'class Timed:' - e ho ancora ottenuto l'errore originale dell'OP. Potresti indicarmi il concetto giusto su cui leggere per capire perché? – scubbo

+0

@scubbo - Eri in Python2.7? In 2.7 si vuole essere sicuri che ogni classe erediti da 'object'; questa ereditarietà è di default in Python3. Non so * specificamente * perché questo si sia manifestato in un bug, ma questo è il probabile colpevole - 'type (YourClass)' dovrebbe essere 'type', non' classobj'. – dwanderson

0

Personnaly, io uso Decorator in questo modo:

def timeit(method): 
    def timed(*args, **kw): 
     ts = time.time() 
     result = method(*args, **kw) 
     te = time.time() 
     ts = round(ts * 1000) 
     te = round(te * 1000) 
     print('%r (%r, %r) %2.2f millisec' % 
      (method.__name__, args, kw, te - ts)) 
     return result 
    return timed 


class whatever(object): 
    @timeit 
    def myfunction(self): 
     do something 
7

Prima devi capire how function become methods and how self is "automagically" injected.

Una volta che sai che, il "problema" è evidente: si sta decorando la funzione decorated con un'istanza Timed - IOW, Test.decorated è un'istanza Timed, non un'istanza function - e la classe Timed non mimare il tipo di function implementazione del protocollo descriptor. Ciò che si vuole è simile al seguente:

import types 

class Timed(object): 
    def __init__(self, f): 
     self.func = f 

    def __call__(self, *args, **kwargs): 
     start = dt.datetime.now() 
     ret = self.func(*args, **kwargs) 
     time = dt.datetime.now() - start 
     ret["time"] = time 
     return ret 

    def __get__(self, instance, cls):   
     return types.MethodType(self, instance, cls) 
+0

ottima risposta e al punto! :-) – jsbueno

+0

Grazie, questo ha aiutato. Una nota però, questo non funziona in Python 3, poiché i metodi non associati non esistono più ('MethodType' accetta solo due argomenti e il secondo non deve essere None). Per Python 3 l'alternativa sarebbe: 'return types.MethodType (self, instance) if instance else self'. –