考虑这个小例子:
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")
它按预期工作。但是,如果我必须事先知道某个函数是否装饰,那么它就无法实现装饰器的全部目的。这里的模式是什么,还是我误会了什么?
最佳答案
tl;博士
您可以通过将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)
实际问题
引用Python文档decorator,
所以,当你说
@Timed
def decorated(self, *args, **kwargs):
它实际上是
decorated = Timed(decorated)
仅将功能对象传递给
Timed
,实际绑定(bind)到该对象的对象不会随一起传递。因此,当您像这样调用它时ret = self.func(*args, **kwargs)
self.func
将引用未绑定(bind)的函数对象,并以Hello
作为第一个参数来调用它。这就是self
打印为Hello
的原因。如何解决此问题?
由于您没有在
Test
中引用Timed
实例,因此唯一的方法是将Timed
转换为描述符类。引用文档Invoking descriptors部分,我们可以通过简单地定义这样的方法来使
Timed
成为描述符def __get__(self, instance, owner):
...
在这里,
self
指的是Timed
对象本身,instance
指的是发生属性查找的实际对象,owner
指的是与instance
对应的类。现在,在
__call__
上调用Timed
时,将调用__get__
方法。现在,以某种方式,我们需要将第一个参数作为Test
类的实例传递(甚至在Hello
之前)。因此,我们创建了另一个部分应用的函数,其第一个参数将是Test
实例,如下所示def __get__(self, instance, owner):
from functools import partial
return partial(self.__call__, instance)
现在,
self.__call__
是一个绑定(bind)方法(绑定(bind)到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>
。