如何用装饰器 class 装饰实例方法?
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
参数(应该是 Test obj 实例)没有作为第一个参数传递给装饰函数 decorated
?
如果我手动执行,例如:
def call_deco(self):
self.decorated(self, "Hello", world="World")
它按预期工作。但是如果我必须事先知道一个函数是否被装饰,它就违背了装饰器的全部目的。走这里的模式是什么,还是我理解错了什么?
你首先要明白how function become methods and how self
is "automagically" injected。
一旦你知道了,"problem" 就很明显了:你正在用 Timed
实例装饰 decorated
函数 - IOW,Test.decorated
是一个 Timed
实例,而不是 function
实例 - 并且您的 Timed
class 不模仿 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)
tl;dr
您可以通过使 Timed
class 成为 descriptor 和 returning 来自 __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 文档
The decorator syntax is merely syntactic sugar, the following two function definitions are semantically equivalent:
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
转换为 描述符 class。引用文档,Invoking descriptors 部分,
In general, a descriptor is an object attribute with “binding behavior”, one whose attribute access has been overridden by methods in the descriptor protocol: __get__()
, __set__()
, and __delete__()
. If any of those methods are defined for an object, it is said to be a descriptor.
The default behavior for attribute access is to get, set, or delete the attribute from an object’s dictionary. For instance, a.x
has a lookup chain starting with a.__dict__['x']
, then type(a).__dict__['x']
, and continuing through the base classes of type(a)
excluding metaclasses.
However, if the looked-up value is an object defining one of the descriptor methods, then Python may override the default behavior and invoke the descriptor method instead.
我们可以使 Timed
成为描述符,只需定义这样的方法
def __get__(self, instance, owner):
...
此处,self
指的是 Timed
对象本身,instance
指的是进行属性查找的实际对象,owner
指的是 class对应的是instance
.
现在,当在 Timed
上调用 __call__
时,将调用 __get__
方法。现在,不知何故,我们需要将第一个参数作为 Test
class 的实例传递(甚至在 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__
方法,并且它 return 是一个 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")
因为我们return一个partial
函数,
partial(TimedObject.__call__, <Test obj>)("Hello", world="World"))
实际上是
TimedObject.__call__(<Test obj>, 'Hello', world="World")
因此,<Test obj>
也成为 *args
的一部分,当调用 self.func
时,第一个参数将是 <Test obj>
.
考虑这个小例子:
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")
它按预期工作。但是如果我必须事先知道一个函数是否被装饰,它就违背了装饰器的全部目的。走这里的模式是什么,还是我理解错了什么?
你首先要明白how function become methods and how self
is "automagically" injected。
一旦你知道了,"problem" 就很明显了:你正在用 Timed
实例装饰 decorated
函数 - IOW,Test.decorated
是一个 Timed
实例,而不是 function
实例 - 并且您的 Timed
class 不模仿 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)
tl;dr
您可以通过使 Timed
class 成为 descriptor 和 returning 来自 __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 文档The decorator syntax is merely syntactic sugar, the following two function definitions are semantically equivalent:
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
转换为 描述符 class。引用文档,Invoking descriptors 部分,
In general, a descriptor is an object attribute with “binding behavior”, one whose attribute access has been overridden by methods in the descriptor protocol:
__get__()
,__set__()
, and__delete__()
. If any of those methods are defined for an object, it is said to be a descriptor.The default behavior for attribute access is to get, set, or delete the attribute from an object’s dictionary. For instance,
a.x
has a lookup chain starting witha.__dict__['x']
, thentype(a).__dict__['x']
, and continuing through the base classes oftype(a)
excluding metaclasses.However, if the looked-up value is an object defining one of the descriptor methods, then Python may override the default behavior and invoke the descriptor method instead.
我们可以使 Timed
成为描述符,只需定义这样的方法
def __get__(self, instance, owner):
...
此处,self
指的是 Timed
对象本身,instance
指的是进行属性查找的实际对象,owner
指的是 class对应的是instance
.
现在,当在 Timed
上调用 __call__
时,将调用 __get__
方法。现在,不知何故,我们需要将第一个参数作为 Test
class 的实例传递(甚至在 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__
方法,并且它 return 是一个 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")
因为我们return一个partial
函数,
partial(TimedObject.__call__, <Test obj>)("Hello", world="World"))
实际上是
TimedObject.__call__(<Test obj>, 'Hello', world="World")
因此,<Test obj>
也成为 *args
的一部分,当调用 self.func
时,第一个参数将是 <Test obj>
.