在python中嵌套描述符/装饰器
Posted
技术标签:
【中文标题】在python中嵌套描述符/装饰器【英文标题】:Nesting descriptors/decorators in python 【发布时间】:2013-03-12 22:56:17 【问题描述】:当我尝试嵌套描述符/装饰器时,我很难理解会发生什么。我正在使用 python 2.7。
以property
和classmethod
的以下简化版本为例:
class MyProperty(object):
def __init__(self, fget):
self.fget = fget
def __get__(self, obj, objtype=None):
print 'IN MyProperty.__get__'
return self.fget(obj)
class MyClassMethod(object):
def __init__(self, f):
self.f = f
def __get__(self, obj, objtype=None):
print 'IN MyClassMethod.__get__'
def f(*args, **kwargs):
return self.f(objtype, *args, **kwargs)
return f
试图嵌套它们:
class A(object):
# doesn't work:
@MyProperty
@MyClassMethod
def klsproperty(cls):
return 555
# works:
@MyProperty
def prop(self):
return 111
# works:
@MyClassMethod
def klsmethod(cls, x):
return x**2
% print A.klsproperty
IN MyProperty.__get__
...
TypeError: 'MyClassMethod' object is not callable
内部描述符MyClassMethod
的__get__
方法没有被调用。
由于无法弄清楚原因,我尝试输入(我认为是)一个无操作描述符:
class NoopDescriptor(object):
def __init__(self, f):
self.f = f
def __get__(self, obj, objtype=None):
print 'IN NoopDescriptor.__get__'
return self.f.__get__(obj, objtype=objtype)
尝试在嵌套中使用无操作描述符/装饰器:
class B(object):
# works:
@NoopDescriptor
@MyProperty
def prop1(self):
return 888
# doesn't work:
@MyProperty
@NoopDescriptor
def prop2(self):
return 999
% print B().prop1
IN NoopDescriptor.__get__
IN MyProperty.__get__
888
% print B().prop2
IN MyProperty.__get__
...
TypeError: 'NoopDescriptor' object is not callable
我不明白为什么 B().prop1
有效而 B().prop2
无效。
问题:
-
我做错了什么?为什么我会收到
object is not callable
错误?
正确的方法是什么?例如在重用MyClassMethod
和MyProperty
(或classmethod
和property
)时定义MyClassProperty
的最佳方式是什么
【问题讨论】:
直观理解这一点的关键是@MyProperty
创建的东西类似于 data 属性,而不是方法,因此您不能将其与方法描述符嵌套.这种直观的理解只能让你走这么远;完整的故事在 isedev 的回答中。
【参考方案1】:
在这种情况下,当使用不带参数的装饰器时,将调用装饰器并将其装饰的函数作为其参数。使用装饰器的返回值而不是装饰函数。所以:
@MyProperty
def prop(self):
...
相当于:
def prop(self):
...
prop = MyProperty(prop)
由于MyProperty
实现了描述符协议,访问A.prop
实际上会调用A.prop.__get__()
,并且您已经定义__get__
来调用被装饰的对象(在本例中为原始函数/方法),所以一切正常。
现在,在嵌套情况下:
@MyProperty
@MyClassMethod
def prop(self):
...
等价于:
def prop(self):
...
prop = MyClassMethod(prop) # prop is now instance of MyClassMethod
prop = MyProperty(prop) # prop is now instance of MyProperty
# (with fget == MyClassMethod instance)
现在,和以前一样,访问A.prop
将实际调用A.prop.__get__()
(在MyProperty
中),然后尝试调用MyClassMethod
的实例(被装饰和存储的对象在fget
属性中)。
但是MyClassMethod
没有定义__call__
方法,所以您会收到错误MyClassMethod is not callable
。
并解决您的第二个问题:属性已经是类属性 - 在您的示例中,访问 A.prop
将返回类对象中的属性值,A().prop
将返回属性值实例对象(如果实例没有覆盖它,它可以与类对象相同)。
【讨论】:
您的解释是否清楚。谢谢你。关于第二个答案:我不确定我理解你的意思。我正在寻找与classmethod
类似的东西,这意味着我可以将其称为A.prop
或A().prop
,并且在这两种情况下,底层函数都传递了cls
arg。
在这种情况下,您可以检查obj
参数的类型:如果它是类实例,您可以从中提取类并将其传递给修饰函数而不是实例对象。
你没有抓住重点......我的问题不在于如何实现 classproperty
装饰器。这是关于嵌套描述符。您建议的逻辑已经在classmethod
中实现。我希望应该有一种干净的方法来重用/组合 property
和 classmethod
来实现这一点,而不是重新实现它们中的逻辑。但我开始认为没有......
但它们不适用于同一事物:property
适用于描述符,classmethod
适用于可调用(方法)——两种不同的协议。您希望如何将它们结合起来?
所以我可能不明白的部分是为什么“属性适用于描述符”。您可以使用foo = property(foo)
,其中foo
充当fget
,而foo
是可调用的,并且在您访问self.foo
时会被调用。我认为这意味着“属性适用于可调用对象”,很像 classmethod
。【参考方案2】:
如果您使MyProperty
将描述符协议应用于其包装对象,您可以使您的代码工作:
class MyProperty(object):
def __init__(self, fget):
self.fget = fget
def __get__(self, obj, objtype=None):
print('IN MyProperty.__get__')
try:
return self.fget.__get__(obj, objtype)()
except AttributeError: # self.fget has no __get__ method
return self.fget(obj)
现在您的示例代码可以工作了:
class A(object):
@MyProperty
@MyClassMethod
def klsproperty(cls):
return 555
print(A.klsproperty)
输出是:
IN MyProperty.__get__
IN MyClassMethod.__get__
555
【讨论】:
谢谢,这是有道理的。但这是推荐的方法吗?如果是这样,为什么内置的property
和 classmethod
不这样做(并且不能嵌套)?
我不确定是否不鼓励这样做,或者为什么内置 property
还没有这样做。我确实知道有一些限制,但这并不能解决。一方面,你不能以相反的顺序嵌套描述符,即使你让MyClassMethod
更聪明。而且我不认为 __set__
描述符函数被调用来进行类变量赋值,所以你需要做一些元类魔法才能使可写的类属性起作用。也许有更多描述性编程经验的人可以权衡其他问题和限制。【参考方案3】:
我在 Graham Dumpleton 引人入胜的 blog 中找到了我自己的老问题的明确答案。
简而言之,我编写的装饰器不尊重描述符协议,而是尝试直接调用包装的函数/对象,而不是首先让它们有机会执行它们的“描述符魔法”(先打电话给他们的__get__()
)。
【讨论】:
以上是关于在python中嵌套描述符/装饰器的主要内容,如果未能解决你的问题,请参考以下文章
32.Python面向对象描述符运算符底层装饰器:闭包-闭包参数-内置装饰器-类装饰器
32.Python面向对象描述符运算符底层装饰器:闭包-闭包参数-内置装饰器-类装饰器
32.Python面向对象描述符运算符底层装饰器:闭包-闭包参数-内置装饰器-类装饰器