简体   繁体   English

如何使用装饰器类装饰实例方法?

[英]How can I decorate an instance method with a decorator class?

Consider this small example:考虑这个小例子:

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

which prints哪个打印

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

Why is the self parameter (which should be the Test obj instance) not passed as first argument to the decorated function decorated ?为什么self参数(这应该是判断obj实例)未通过的第一个参数功能,装饰decorated

If I do it manually, like :如果我手动执行,例如:

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

it works as expected.它按预期工作。 But if I must know in advance if a function is decorated or not, it defeats the whole purpose of decorators.但是如果我必须事先知道一个函数是否被装饰,它就违背了装饰器的全部目的。 What is the pattern to go here, or do I misunderstood something?去这里的模式是什么,或者我误解了什么?

tl;dr tl;博士

You can fix this problem by making the Timed class a descriptor and returning a partially applied function from __get__ which applies the Test object as one of the arguments, like this您可以通过使Timed类成为描述符并从__get__返回一个部分应用的函数来解决此问题,该函数将Test对象作为参数之一应用,如下所示

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)

The actual problem实际问题

Quoting Python documentation for decorator ,引用装饰器的Python 文档,

The decorator syntax is merely syntactic sugar, the following two function definitions are semantically equivalent:装饰器语法只是语法糖,以下两个函数定义在语义上是等效的:

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

So, when you say,所以,当你说,

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

it is actually它实际上是

decorated = Timed(decorated)

only the function object is passed to the Timed , the object to which it is actually bound is not passed on along with it .只有函数对象被传递给Timed它实际绑定的对象不会随之传递 So, when you invoke it like this所以,当你像这样调用它时

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

self.func will refer to the unbound function object and it is invoked with Hello as the first argument. self.func将引用未绑定的函数对象,并以Hello作为第一个参数调用它。 That is why self prints as Hello .这就是为什么self打印为Hello


How can I fix this?我怎样才能解决这个问题?

Since you have no reference to the Test instance in the Timed , the only way to do this would be to convert Timed as a descriptor class .由于您没有对TimedTest实例的引用,因此唯一的方法是将Timed转换为描述符类 Quoting the documentation, Invoking descriptors section,引用文档, 调用描述符部分,

In general, a descriptor is an object attribute with “binding behavior”, one whose attribute access has been overridden by methods in the descriptor protocol: __get__() , __set__() , and __delete__() .通常,描述符是具有“绑定行为”的对象属性,其属性访问已被描述符协议中的方法覆盖: __get__()__set__()__delete__() If any of those methods are defined for an object, it is said to be a descriptor.如果为对象定义了这些方法中的任何一个,则称其为描述符。

The default behavior for attribute access is to get, set, or delete the attribute from an object's dictionary.属性访问的默认行为是从对象的字典中获取、设置或删除属性。 For instance, ax has a lookup chain starting with a.__dict__['x'] , then type(a).__dict__['x'] , and continuing through the base classes of type(a) excluding metaclasses.例如, ax有一个以a.__dict__['x']开始的查找链,然后是type(a).__dict__['x'] ,并继续遍历type(a)的基类,不包括元类。

However, if the looked-up value is an object defining one of the descriptor methods, then Python may override the default behavior and invoke the descriptor method instead .但是,如果查找值是定义描述符方法之一的对象,则 Python 可能会覆盖默认行为并改为调用描述符方法

We can make Timed a descriptor, by simply defining a method like this我们可以通过简单地定义一个像这样的方法来使Timed成为一个描述符

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

Here, self refers to the Timed object itself, instance refers to the actual object on which the attribute lookup is happening and owner refers to the class corresponding to the instance .这里, self指的是Timed对象本身, instance指的是发生属性查找的实际对象, owner指的是与instance对应的类。

Now, when __call__ is invoked on Timed , the __get__ method will be invoked.现在,当在Timed上调用__call__ Timed ,将调用__get__方法。 Now, somehow, we need to pass the first argument as the instance of Test class (even before Hello ).现在,不知何故,我们需要将第一个参数作为Test类的实例(甚至在Hello之前)传递。 So, we create another partially applied function, whose first parameter will be the Test instance, like this所以,我们创建了另一个部分应用的函数,它的第一个参数是Test实例,像这样

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

Now, self.__call__ is a bound method (bound to Timed instance) and the second parameter to partial is the first argument to the self.__call__ call.现在, self.__call__是一个绑定方法(绑定到Timed实例), partial的第二个参数是self.__call__调用的第一个参数。

So, all these effectively translate like this所以,所有这些都有效地翻译成这样

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

Now self.decorated is actually Timed(decorated) (this will be referred as TimedObject from now on) object.现在self.decorated实际上是Timed(decorated) (从现在开始将被称为TimedObject )对象。 Whenever we access it, the __get__ method defined in it will be invoked and it returns a partial function.每当我们访问它时,就会调用其中定义的__get__方法并返回一个partial函数。 You can confirm that like this你可以像这样确认

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

would print会打印

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

So,所以,

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

gets translated to被翻译成

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

Since we return a partial function,由于我们返回一个partial函数,

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

which is actually这实际上是

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

So, <Test obj> also becomes a part of *args , and when self.func is invoked, the first argument will be the <Test obj> .因此, <Test obj>也成为*args的一部分,当self.func被调用时,第一个参数将是<Test obj>

You first have to understand how function become methods and how self is "automagically" injected .您首先必须了解function 如何成为方法以及self如何“自动”注入

Once you know that, the "problem" is obvious: you are decorating the decorated function with a Timed instance - IOW, Test.decorated is a Timed instance, not a function instance - and your Timed class does not mimick the function type's implementation of the descriptor protocol.一旦你知道,“问题” Test.decorated明显了:你正在用一个Timed实例装饰decorated函数 - IOW, Test.decorated是一个Timed实例,而不是一个function实例 - 你的Timed类不会模仿function类型的实现descriptor协议。 What you want looks like this:你想要的看起来像这样:

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)

I use decorators in the following 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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM