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

47
考虑这个小例子:
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'}

为什么被装饰的函数decorated的第一个参数不是应该是Test对象实例的self参数?
如果我手动添加,像这样:
def call_deco(self):
    self.decorated(self, "Hello", world="World")

它按预期工作。但如果我必须事先知道一个函数是否被装饰,那么这就违背了装饰器的整个目的。在这里应该采用什么模式,或者我有误解吗?


2
快速谷歌搜索可以找到这个链接:http://thecodeship.com/patterns/guide-to-python-function-decorators/(请参见“装饰方法”部分)。 - cfh
1
你读过例如 https://dev59.com/jnE95IYBdhLWcg3wUMLW,https://dev59.com/527Xa4cB1Zd3GeqPvvSO 吗? - jonrsharpe
1
当您使用函数作为装饰器而不是可调用对象时,您将不会遇到这种问题。 - poke
3个回答

65

简而言之

您可以通过将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)

实际问题

引用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的原因。


我该如何修复这个问题?
由于在“定时”中没有对“测试”实例的引用,唯一的方法是将“定时”转换为描述符类。引用文档中的调用描述符部分。
一般来说,描述符是一个具有“绑定行为”的对象属性,其属性访问已被描述符协议中的方法所覆盖:__get__()、__set__()和__delete__()。如果任何这些方法都为一个对象定义了,那么它就被称为描述符。
属性访问的默认行为是从对象字典中获取、设置或删除属性。例如,a.x具有以a.__dict__['x']开始的查找链,然后是type(a).__dict__['x'],并且继续通过排除元类的type(a)的基类。
但是,如果查找到的值是定义了描述符方法之一的对象,则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 的第二个参数是传递给 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>

2
为什么要使用 functool.partial 而不是内置的专用 types.MethodType? - bruno desthuilliers
请问您能否解释一下为什么以下代码可以工作? def wrapper(*args): print(args[0].url) return f(*args) return wrapper class Client(object): def __init__(self, url): self.url = url @check_authorization def get(self): print('get') 这段代码是如何工作的?它不会也是未绑定的吗? - dragonxlwang
1
我一直在努力让这个工作,但是当装饰器需要参数时就无法实现。你有什么想法可以使其与例如@Timed(interval=5)一起正常工作吗? - matt murray
1
有没有办法向 Timed 对象添加参数,以便我们可以执行类似于 @Timed(some_init_arg) 的操作? - Pablo
我刚刚复制了你的驱动器,希望它能正常工作 https://colab.research.google.com/drive/1UOTgYsdNpRpdEA16Rl7Jj8LXTYdQI1Zd#scrollTo=-pkuq55Xsmi9 @PythonF - Avinash Raj
显示剩余4条评论

13

首先,您需要了解函数如何变成方法以及如何“自动地”注入self

一旦您知道了这一点,问题便显而易见:您正在用一个Timed实例装饰decorated函数 - 换句话说,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)

5
谢谢,这有帮助。不过要注意,这在Python 3中不起作用,因为无绑定方法已经不存在了(MethodType只接受两个参数,并且第二个参数不能为None)。对于Python 3,替代方法是:return types.MethodType(self, instance) if instance else self - Dário
1
@Dário 可能应该是 if instance is not None else。不想因为 instance 恰好为假值而触发 else - Dominick Pastore
的确 @DominickPastore。 - Dário

2

我结合了一些答案和评论,尤其是来自 @PythonF 的,他提供的谷歌协作链接对于了解不同方法的实际工作方式非常有帮助。我的目标不是成为最好的答案,因为其他人知道得更好,但尽管有很多其他很棒的答案,没有人真正用完整且可复用的代码回答了这个问题,所以这里有一些包含测试用例的简洁代码。

这可以接受参数并正确传递实例:

    class Decorator:
        def __init__(self, func = None, start_text = "Start", stop_text = "Stop"):
            self.func = func
            self.instance = None
            self.start_text = start_text
            self.stop_text = stop_text
    
        def __call__(self, func):
            if self.func is None:
                self.func = func
            def call(*args, **kwargs):
                if self.instance is None and len(args) > 0:
                    self.instance = args[0]
                # do stuff before
                print(f"--- {self.start_text} ---")
                wrapped_method = self.func(self.instance, *args[1:], **kwargs)
                # do stuff afterwards
                print(f"--- {self.stop_text} ---")
                return wrapped_method
            return call
    

    class HelloWorld:
        def __init__(self):
            self.test = "test"
    
        @Decorator(start_text="Starting...", stop_text="Done")
        def print(self, name):
            print(name)
            print(self.test)
            return 42


    hello_world = HelloWorld()
    hello_world.print("Max Musterman")

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接