如何用装饰器 class 装饰实例方法?

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

考虑这个小例子:

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

它按预期工作。但是如果我必须事先知道一个函数是否被装饰,它就违背了装饰器的全部目的。走这里的模式是什么,还是我理解错了什么?

你首先要明白how function become methods and how self is "automagically" injected

一旦你知道了,"problem" 就很明显了:你正在用 Timed 实例装饰 decorated 函数 - IOW,Test.decorated 是一个 Timed 实例,而不是 function 实例 - 并且您的 Timed class 不模仿 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)

tl;dr

您可以通过使 Timed class 成为 descriptor 和 returning 来自 __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)

实际问题

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

所以,当你说,

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

其实是

decorated = Timed(decorated)

只有函数对象被传递给Timed它实际绑定到的对象并没有随之传递。所以,当你像这样调用它时

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

self.func 将引用未绑定的函数对象,它以 Hello 作为第一个参数调用。这就是 self 打印为 Hello.

的原因

我该如何解决这个问题?

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

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__(). 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, a.x 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.

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.

我们可以使 Timed 成为描述符,只需定义这样的方法

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

此处,self 指的是 Timed 对象本身,instance 指的是进行属性查找的实际对象,owner 指的是 class对应的是instance.

现在,当在 Timed 上调用 __call__ 时,将调用 __get__ 方法。现在,不知何故,我们需要将第一个参数作为 Test class 的实例传递(甚至在 Hello 之前)。因此,我们创建另一个部分应用的函数,其第一个参数将是 Test 实例,例如

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

现在,self.__call__ 是一个绑定方法(绑定到 Timed 实例),partial 的第二个参数是 self.__call__ 调用的第一个参数。

所以,所有这些有效地翻译成这样

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

现在self.decorated实际上是Timed(decorated)(以后称为TimedObject)对象。每当我们访问它时,都会调用其中定义的 __get__ 方法,并且它 return 是一个 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")

因为我们return一个partial函数,

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

实际上是

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

因此,<Test obj> 也成为 *args 的一部分,当调用 self.func 时,第一个参数将是 <Test obj>.