【问题标题】:How can I decorate an instance method with a decorator class?如何用装饰器类装饰实例方法?
【发布时间】:2019-02-03 03:28:42
【问题描述】:

考虑这个小例子:

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

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

【问题讨论】:

标签: python class self python-decorators


【解决方案1】:

tl;dr

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

实际问题

引用decorator 的 Python 文档,

装饰器语法只是语法糖,以下两个函数定义在语义上是等价的:

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 转换为描述符类。引用文档,Invoking descriptors 部分,

通常,描述符是具有“绑定行为”的对象属性,其属性访问已被描述符协议中的方法覆盖:__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")

所以,&lt;Test obj&gt; 也成为*args 的一部分,当调用self.func 时,第一个参数将是&lt;Test obj&gt;

【讨论】:

  • 为什么使用 functool.partial 而不是内置的专用 types.MethodType
  • 感谢您提供如此全面的答案 - 我将不得不阅读它几次才能了解它,但我相信我会的!当我第一次尝试应用它时,我在装饰器的定义中省略了 (object) - 所以,我只有 class Timed: - 我仍然得到了 OP 的原始错误。您能否指出正确的概念以供阅读以了解原因?
  • @scubbo - 你在 Python2.7 中吗?在 2.7 中,您要确保每个类都继承自 object;这种继承在 Python3 中是默认的。我不知道具体为什么会出现在错误中,但这可能是罪魁祸首 - type(YourClass) 应该是 type,而不是 classobj
  • 当你有一个接受参数的装饰器时,我一直在努力让它工作。您对如何使用它有任何想法,例如@Timed(interval=5)
  • 有没有办法向Timed 对象添加参数,以便我们可以执行@Timed(some_init_arg) 之类的操作?
【解决方案2】:

你首先要了解how function become methods and how self is "automagically" injected

一旦您知道这一点,“问题”就很明显了:您正在使用 Timed 实例装饰 decorated 函数 - 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)

【讨论】:

  • 谢谢,这有帮助。不过需要注意的是,这在 Python 3 中不起作用,因为不再存在未绑定的方法(MethodType 只接受两个参数,第二个不能为 None)。对于 Python 3,替代方法是:return types.MethodType(self, instance) if instance else self.
  • @Dário 那应该是if instance is not None else。不想因为instance 恰好是假的而触发else
  • 确实是@DominickPastore。
猜你喜欢
  • 2017-11-12
  • 2020-01-11
  • 2014-01-14
相关资源
最近更新 更多