考虑这个小例子:
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)
实际问题
引用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
。
我怎样才能解决这个问题?
由于你没有引用Test
中的Timed
实例,唯一的方法是将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
的类。
现在,当在__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>
。
你首先必须了解how function become methods and how self
is "automagically" injected。
一旦你知道,“问题”是显而易见的:你用decorated
实例装饰Timed
函数 - 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