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

发布于 2021-01-29 15:06:04

考虑这个小例子:

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

它按预期工作。但是,如果我必须事先知道某个函数是否装饰,它就破坏了装饰器的全部目的。这里的模式是什么,还是我误会了什么?

关注者
0
被浏览
81
1 个回答
  • 面试哥
    面试哥 2021-01-29
    为面试而生,有面试问题,就找面试哥。

    tl; dr

    您可以通过将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文档作为 decorator

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

    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


    我怎样才能解决这个问题?

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

    一般而言,描述符是与“结合行为”,一个属性的访问已被覆盖通过在描述符协议方法的对象属性:__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

    现在,在__call__上调用时Timed,该__get__方法将被调用。现在,以某种方式,我们需要将第一个参数传递为Testclass的实例(甚至在之前Hello)。因此,我们创建了另一个部分应用的函数,其第一个参数将是Test实例,如下所示

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

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



知识点
面圈网VIP题库

面圈网VIP题库全新上线,海量真题题库资源。 90大类考试,超10万份考试真题开放下载啦

去下载看看