繁体   English   中英

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

[英]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参数(这应该是判断obj实例)未通过的第一个参数功能,装饰decorated

如果我手动执行,例如:

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

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

tl;博士

您可以通过使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 文档,

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

 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


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

由于您没有对TimedTest实例的引用,因此唯一的方法是将Timed转换为描述符类 引用文档, 调用描述符部分,

通常,描述符是具有“绑定行为”的对象属性,其属性访问已被描述符协议中的方法覆盖: __get__()__set__()__delete__() 如果为对象定义了这些方法中的任何一个,则称其为描述符。

属性访问的默认行为是从对象的字典中获取、设置或删除属性。 例如, ax有一个以a.__dict__['x']开始的查找链,然后是type(a).__dict__['x'] ,并继续遍历type(a)的基类,不包括元类。

但是,如果查找值是定义描述符方法之一的对象,则 Python 可能会覆盖默认行为并改为调用描述符方法

我们可以通过简单地定义一个像这样的方法来使Timed成为一个描述符

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

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

现在,当在Timed上调用__call__ Timed ,将调用__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>

您首先必须了解function 如何成为方法以及self如何“自动”注入

一旦你知道,“问题” Test.decorated明显了:你正在用一个Timed实例装饰decorated函数 - IOW, 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)

我通过以下方式使用装饰器:

def timeit(method):
    def timed(*args, **kw):
        ts = time.time()
        result = method(*args, **kw)
        te = time.time()
        ts = round(ts * 1000)
        te = round(te * 1000)
        print('%r (%r, %r) %2.2f millisec' %
             (method.__name__, args, kw, te - ts))
        return result
    return timed


 class whatever(object):
    @timeit
    def myfunction(self):
         do something

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM