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?
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 Timed
jako klasy deskryptora . Cytując dokumentację, wywołując sekcję deskryptorów ,
Możemy stworzyć deskryptor, po prostu definiując metodę taką jak ta]}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ę oda.__dict__['x']
, następnietype(a).__dict__['x']
i kontynuujący przez klasy bazowetype(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 .
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>
.
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)
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
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