web-dev-qa-db-ja.com

インスタンスメソッドをデコレータクラスで装飾するにはどうすればよいですか?

この小さな例を考えてみましょう:

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

期待どおりに動作します。しかし、関数が装飾されているかどうかを事前に知っておく必要がある場合、それは装飾子の目的全体を無効にします。ここに行くパターンは何ですか、または何かを誤解していますか?

33
Rafael T

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を最初の引数として呼び出されます。そのため、selfHelloとして出力されます。


どうすれば修正できますか?

TestTimedインスタンスへの参照がないため、これを行う唯一の方法は、Timed記述子クラスとして変換することです。ドキュメントを引用して、 記述子の呼び出し セクション、

一般に、記述子は「バインディング動作」を備えたオブジェクト属性であり、その属性アクセスは、記述子プロトコルのメソッド__get__()__set__()、および__delete__()。これらのメソッドのいずれかがオブジェクトに対して定義されている場合、それは記述子と呼ばれます。

属性アクセスのデフォルトの動作は、オブジェクトのディクショナリから属性を取得、設定、または削除することです。たとえば、_a.x_には、_a.__dict__['x']_で始まり、次にtype(a).__dict__['x']で始まり、メタクラスを除くtype(a)の基本クラスまで続くルックアップチェーンがあります。

ただし、ルックアップされた値が記述子メソッドの1つを定義するオブジェクトである場合、Pythonはデフォルトの動作をオーバーライドして、代わりに記述子メソッドを呼び出すことができます

このようにメソッドを定義するだけで、Timedを記述子にすることができます

_def __get__(self, instance, owner):
    ...
_

ここで、selfTimedオブジェクト自体を指し、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>_になります。

38
thefourtheye

最初に 関数がメソッドになる方法とselfが「自動的に」注入される方法 を理解する必要があります。

それがわかったら、「問題」は明白です。decorated関数をTimedインスタンスで装飾しています-IOW、Test.decoratedTimedインスタンス、 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)
9

個人的に、私は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
0
PyNico