如何使用装饰器类装饰实例方法? [英] 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
?
Why is the self
parameter (which should be the Test obj instance) not passed as first argument to the decorated function decorated
?
如果我手动执行,例如:
If I do it manually, like :
def call_deco(self):
self.decorated(self, "Hello", world="World")
它按预期工作.但是如果我必须事先知道一个函数是否被装饰,它就违背了装饰器的全部目的.去这里的模式是什么,或者我误解了什么?
it works as expected. But if I must know in advance if a function is decorated or not, it defeats the whole purpose of decorators. What is the pattern to go here, or do I misunderstood something?
推荐答案
tl;dr
您可以通过将 Timed
类设为 descriptor 并从 __get__
返回一个部分应用的函数,它应用 Test
对象作为参数之一,就像这样
You can fix this problem by making the Timed
class a descriptor and returning a partially applied function from __get__
which applies the Test
object as one of the arguments, like this
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 文档,
Quoting Python documentation for decorator,
装饰器语法只是语法糖,以下两个函数定义在语义上是等价的:
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
,它实际绑定的对象不会随之传递.所以,当你像这样调用它
only the function object is passed to the Timed
, the object to which it is actually bound is not passed on along with it. So, when you invoke it like this
ret = self.func(*args, **kwargs)
self.func
将引用未绑定的函数对象,并以 Hello
作为第一个参数调用它.这就是为什么 self
打印为 Hello
.
self.func
will refer to the unbound function object and it is invoked with Hello
as the first argument. That is why self
prints as Hello
.
我该如何解决这个问题?
由于您没有引用 Timed
中的 Test
实例,唯一的方法是将 Timed
转换为描述符类.引用文档,调用描述符部分,
Since you have no reference to the Test
instance in the Timed
, the only way to do this would be to convert Timed
as a descriptor class. Quoting the documentation, Invoking descriptors section,
一般而言,描述符是具有绑定行为"的对象属性,其属性访问已被描述符协议中的方法覆盖:__get__()
, __set__()
和 __delete__()
.如果为一个对象定义了这些方法中的任何一个,则称其为描述符.
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.
属性访问的默认行为是从对象的字典中获取、设置或删除属性.例如,ax
有一个以 a.__dict__['x']
开头的查找链,然后是 type(a).__dict__['x']
code>,并继续通过 type(a)
的基类,不包括元类.
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.
但是,如果查找的值是定义描述符方法之一的对象,则 Python 可能会覆盖默认行为并改为调用描述符方法.
我们可以让 Timed
成为一个描述符,只需定义一个像这样的方法
We can make Timed
a descriptor, by simply defining a method like this
def __get__(self, instance, owner):
...
这里,self
指的是 Timed
对象本身,instance
指的是发生属性查找的实际对象,owner
指的是 instance
对应的类.
Here, self
refers to the Timed
object itself, instance
refers to the actual object on which the attribute lookup is happening and owner
refers to the class corresponding to the instance
.
现在,当在 Timed
上调用 __call__
时,将调用 __get__
方法.现在,不知何故,我们需要将第一个参数作为 Test
类的实例(甚至在 Hello
之前)传递.所以,我们创建了另一个部分应用函数,它的第一个参数是 Test
实例,就像这样
Now, when __call__
is invoked on Timed
, the __get__
method will be invoked. Now, somehow, we need to pass the first argument as the instance of Test
class (even before Hello
). So, we create another partially applied function, whose first parameter will be the Test
instance, like this
def __get__(self, instance, owner):
from functools import partial
return partial(self.__call__, instance)
现在,self.__call__
是一个绑定方法(绑定到 Timed
实例),partial
的第二个参数是第一个参数self.__call__
调用.
Now, self.__call__
is a bound method (bound to Timed
instance) and the second parameter to partial
is the first argument to the self.__call__
call.
所以,所有这些都有效地翻译成这样
So, all these effectively translate like this
t.call_deco()
self.decorated("Hello", world="World")
现在 self.decorated
实际上是 Timed(decorated)
(从现在开始将被称为 TimedObject
)对象.每当我们访问它时,将调用其中定义的 __get__
方法并返回一个 partial
函数.你可以这样确认
Now self.decorated
is actually Timed(decorated)
(this will be referred as TimedObject
from now on) object. Whenever we access it, the __get__
method defined in it will be invoked and it returns a partial
function. You can confirm that like this
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
函数,
Since we return a partial
function,
partial(TimedObject.__call__, <Test obj>)("Hello", world="World"))
实际上是
TimedObject.__call__(<Test obj>, 'Hello', world="World")
所以,
也成为了*args
的一部分,当self.func
被调用时,第一个参数将是
.
So, <Test obj>
also becomes a part of *args
, and when self.func
is invoked, the first argument will be the <Test obj>
.
这篇关于如何使用装饰器类装饰实例方法?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!