Jak mogę udekorować metodę instancji klasą dekoratora?

Rozważ ten mały przykład:

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

Które drukuje

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

Dlaczego parametr self (który powinien być testową instancją obj) nie jest przekazywany jako pierwszy argument do funkcji decorated?

Jeśli zrobię to ręcznie, jak:

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

Działa zgodnie z oczekiwaniami. Ale jeśli muszę wiedzieć z góry, czy funkcja jest dekorowana, czy nie, to pokonuje cały cel dekoratorów. Co to za wzór, czy coś źle zrozumiałam?

Author: thefourtheye, 2015-05-07

3 answers

Tl; dr

Możesz rozwiązać ten problem, konwertując deskryptor Timed klasy A i zwracając częściowo zastosowaną funkcję z __get__, która stosuje obiekt Test jako jeden z argumentów, jak to

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)

Faktyczny problem

Cytowanie dokumentacji Pythona dla dekorator,

Składnia dekoratora jest jedynie cukrem składniowym, następujące dwie definicje funkcji są semantycznie odpowiednik:

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

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

Więc, kiedy mówisz,

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

To jest rzeczywiście

decorated = Timed(decorated)

Tylko obiekt function jest przekazywany do Timed, obiekt, z którym jest faktycznie związany, nie jest przekazywany wraz z nim . Tak więc, kiedy przywołujesz to w ten sposób

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

self.func będzie odwoływał się do niezwiązanego obiektu funkcji i jest wywoływany z Hello jako pierwszym argumentem. Dlatego self drukuje jako Hello.


Jak mogę naprawić to?

Ponieważ nie masz odniesienia do instancji Test w Timed, jedynym sposobem na to jest konwersja Timedjako klasy deskryptora . Cytując dokumentację, wywołując sekcję deskryptorów ,

Ogólnie rzecz biorąc, deskryptor jest atrybutem obiektu z "zachowaniem wiązania", którego dostęp do atrybutów został nadpisany przez metody w protokole deskryptora: __get__(), __set__(), i __delete__(). Jeśli którakolwiek z tych metod jest zdefiniowana dla obiekt, mówi się, że jest deskryptorem.

Domyślnym zachowaniem dostępu do atrybutów jest pobieranie, ustawianie lub usuwanie atrybutu ze słownika obiektu. Na przykład, a.x ma łańcuch wyszukiwania zaczynający się od a.__dict__['x'], następnie type(a).__dict__['x'] i kontynuujący przez klasy bazowe type(a) z wyłączeniem metaklas.

Jednakże, jeśli wyszukiwana wartość jest obiektem definiującym jedną z metod deskryptora, Python może nadpisać domyślne zachowanie i wywołać deskryptor metoda zamiast .

Możemy stworzyć deskryptor, po prostu definiując metodę taką jak ta]}
def __get__(self, instance, owner):
    ...

Tutaj, self odnosi się do samego obiektu Timed, instance odnosi się do rzeczywistego obiektu, na którym odbywa się wyszukiwanie atrybutów, A owner odnosi się do klasy odpowiadającej instance.

Teraz, gdy {[38] } jest wywoływana na Timed, zostanie wywołana metoda __get__. W jakiś sposób musimy przekazać pierwszy argument jako instancję klasy Test (nawet przed Hello). Tworzymy więc kolejną częściowo zastosowaną funkcję, której pierwszym parametrem będzie instancja Test, jak ta

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

Teraz, self.__call__ jest metodą bound (związaną z instancją Timed), a drugi parametr partial jest pierwszym argumentem wywołania self.__call__.

Więc, wszystkie te skutecznie tłumaczyć tak

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

Teraz self.decorated jest rzeczywiście Timed(decorated) (od teraz będzie to określane jako TimedObject) obiekt. Ilekroć uzyskujemy do niego dostęp, zdefiniowana w nim metoda __get__ zostanie wywołana i zwróci funkcję partial. Możesz to potwierdzić w ten sposób

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

Drukuje

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

Więc,

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

Zostaje przetłumaczone na

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

Ponieważ zwracamy partial funkcję,

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

Czyli faktycznie

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

Tak więc, <Test obj> staje się również częścią *args, a gdy zostanie wywołana self.func, pierwszym argumentem będzie <Test obj>.

 21
Author: thefourtheye,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-07 16:17:31

Najpierw musisz zrozumieć Jak funkcje stają się metodami i jak self jest "automagicznie" wstrzykiwany .

Gdy już o tym wiesz," problem " jest oczywisty: dekorujesz decorated funkcję instancją Timed - IOW, Test.decorated jest instancją Timed, a nie instancją function - a twoja klasa Timed nie naśladuje implementacji protokołu function. To co chcesz wygląda tak:

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)
 7
Author: bruno desthuilliers,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-07 15:05:28

Personnally, I use Decorator that way:

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
 0
Author: PyNico,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-07 14:57:25