考虑这个小例子:

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;博士

您可以通过将Timed类设置为descriptor并从__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)

实际问题

引用Python文档decorator



所以,当你说
@Timed
def decorated(self, *args, **kwargs):

它实际上是
decorated = Timed(decorated)

仅将功能对象传递给Timed实际绑定(bind)到该对象的对象不会随一起传递。因此,当您像这样调用它时
ret = self.func(*args, **kwargs)
self.func将引用未绑定(bind)的函数对象,并以Hello作为第一个参数来调用它。这就是self打印为Hello的原因。

如何解决此问题?

由于您没有在Test中引用Timed实例,因此唯一的方法是将Timed转换为描述符类。引用文档Invoking descriptors部分,



我们可以通过简单地定义这样的方法来使Timed成为描述符
def __get__(self, instance, owner):
    ...

在这里,self指的是Timed对象本身,instance指的是发生属性查找的实际对象,owner指的是与instance对应的类。

现在,在__call__上调用Timed时,将调用__get__方法。现在,以某种方式,我们需要将第一个参数作为Test类的实例传递(甚至在Hello之前)。因此,我们创建了另一个部分应用的函数,其第一个参数将是Test实例,如下所示
def __get__(self, instance, owner):
    from functools import partial
    return partial(self.__call__, instance)

现在,self.__call__是一个绑定(bind)方法(绑定(bind)到Timed实例),并且partial的第二个参数是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>

10-07 15:15