この小さな例を考えてみましょう:
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()
印刷する
Hello
()
{'world': 'World'}
self
パラメータ(Test objインスタンスである必要があります)が、装飾された関数decorated
の最初の引数として渡されないのはなぜですか?
私がそれを手動で行う場合:
def call_deco(self):
self.decorated(self, "Hello", world="World")
期待どおりに動作します。しかし、関数が装飾されているかどうかを事前に知っておく必要がある場合、それは装飾子の目的全体を無効にします。ここに行くパターンは何ですか、または何かを誤解していますか?
tl; dr
この問題を解決するには、Timed
クラスを descriptor にして、Test
オブジェクトを1つとして適用する___get__
_から部分的に適用された関数を返します。このような引数
_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)
_
実際の問題
引用Pythonドキュメント decorator 、
デコレータの構文は単なる構文上の砂糖であり、次の2つの関数定義は意味的に同等です。
_def f(...): ... f = staticmethod(f) @staticmethod def f(...): ...
_
だから、あなたが言うとき、
_@Timed
def decorated(self, *args, **kwargs):
_
それは実際に
_decorated = Timed(decorated)
_
関数オブジェクトのみがTimed
に渡されます。実際にバインドされているオブジェクトは一緒に渡されません。したがって、このように呼び出すと
_ret = self.func(*args, **kwargs)
_
_self.func
_はバインドされていない関数オブジェクトを参照し、Hello
を最初の引数として呼び出されます。そのため、self
はHello
として出力されます。
どうすれば修正できますか?
Test
のTimed
インスタンスへの参照がないため、これを行う唯一の方法は、Timed
を記述子クラスとして変換することです。ドキュメントを引用して、 記述子の呼び出し セクション、
一般に、記述子は「バインディング動作」を備えたオブジェクト属性であり、その属性アクセスは、記述子プロトコルのメソッド
__get__()
、__set__()
、および__delete__()
。これらのメソッドのいずれかがオブジェクトに対して定義されている場合、それは記述子と呼ばれます。属性アクセスのデフォルトの動作は、オブジェクトのディクショナリから属性を取得、設定、または削除することです。たとえば、_
a.x
_には、_a.__dict__['x']
_で始まり、次にtype(a).__dict__['x']
で始まり、メタクラスを除くtype(a)
の基本クラスまで続くルックアップチェーンがあります。ただし、ルックアップされた値が記述子メソッドの1つを定義するオブジェクトである場合、Pythonはデフォルトの動作をオーバーライドして、代わりに記述子メソッドを呼び出すことができます。
このようにメソッドを定義するだけで、Timed
を記述子にすることができます
_def __get__(self, instance, owner):
...
_
ここで、self
はTimed
オブジェクト自体を指し、instance
は属性ルックアップが行われている実際のオブジェクトを指し、owner
は対応するクラスを指しますinstance
に。
これで、Timed
で___call__
_が呼び出されると、___get__
_メソッドが呼び出されます。では、どういうわけか、最初の引数をTest
クラスのインスタンスとして渡す必要があります(Hello
の前でも)。したがって、次のように、最初のパラメータがTest
インスタンスになる、部分的に適用された別の関数を作成します
_def __get__(self, instance, owner):
from functools import partial
return partial(self.__call__, instance)
_
ここで、_self.__call__
_はバインドされたメソッド(Timed
インスタンスにバインド)であり、partial
の2番目のパラメーターは_self.__call__
_呼び出しの最初の引数です。
だから、これらすべてはこのように効果的に翻訳されます
_t.call_deco()
self.decorated("Hello", world="World")
_
現在、_self.decorated
_は実際にはTimed(decorated)
(これはTimedObject
と呼ばれます)オブジェクトです。アクセスするたびに、それに定義されている___get__
_メソッドが呼び出され、partial
関数が返されます。このように確認できます
_def call_deco(self):
print(self.decorated)
self.decorated("Hello", world="World")
_
印刷する
_<functools.partial object at 0x7fecbc59ad60>
...
_
そう、
_self.decorated("Hello", world="World")
_
に翻訳されます
_Timed.__get__(TimedObject, <Test obj>, Test.__class__)("Hello", world="World")
_
partial
関数を返すので、
_partial(TimedObject.__call__, <Test obj>)("Hello", world="World"))
_
実際には
_TimedObject.__call__(<Test obj>, 'Hello', world="World")
_
したがって、_<Test obj>
_も_*args
_の一部となり、_self.func
_が呼び出されると、最初の引数は_<Test obj>
_になります。
最初に 関数がメソッドになる方法とself
が「自動的に」注入される方法 を理解する必要があります。
それがわかったら、「問題」は明白です。decorated
関数をTimed
インスタンスで装飾しています-IOW、Test.decorated
はTimed
インスタンス、 function
インスタンスではない-そしてTimed
クラスはfunction
タイプのdescriptor
プロトコルの実装を模倣しません。あなたが欲しいものは次のようになります:
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)
個人的に、私はDecoratorをそのように使用しています:
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