在python中嵌套描述符/装饰器

Posted

技术标签:

【中文标题】在python中嵌套描述符/装饰器【英文标题】:Nesting descriptors/decorators in python 【发布时间】:2013-03-12 22:56:17 【问题描述】:

当我尝试嵌套描述符/装饰器时,我很难理解会发生什么。我正在使用 python 2.7。

propertyclassmethod的以下简化版本为例:

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 错误? 正确的方法是什么?例如在重用MyClassMethodMyProperty(或classmethodproperty)时定义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.propA().prop,并且在这两种情况下,底层函数都传递了cls arg。 在这种情况下,您可以检查obj 参数的类型:如果它是类实例,您可以从中提取类并将其传递给修饰函数而不是实例对象。 你没有抓住重点......我的问题不在于如何实现 classproperty 装饰器。这是关于嵌套描述符。您建议的逻辑已经在classmethod 中实现。我希望应该有一种干净的方法来重用/组合 propertyclassmethod 来实现这一点,而不是重新实现它们中的逻辑。但我开始认为没有...... 但它们不适用于同一事物: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

【讨论】:

谢谢,这是有道理的。但这是推荐的方法吗?如果是这样,为什么内置的 propertyclassmethod 不这样做(并且不能嵌套)? 我不确定是否不鼓励这样做,或者为什么内置 property 还没有这样做。我确实知道有一些限制,但这并不能解决。一方面,你不能以相反的顺序嵌套描述符,即使你让MyClassMethod 更聪明。而且我不认为 __set__ 描述符函数被调用来进行类变量赋值,所以你需要做一些元类魔法才能使可写的类属性起作用。也许有更多描述性编程经验的人可以权衡其他问题和限制。【参考方案3】:

我在 Graham Dumpleton 引人入胜的 blog 中找到了我自己的老问题的明确答案。

简而言之,我编写的装饰器不尊重描述符协议,而是尝试直接调用包装的函数/对象,而不是首先让它们有机会执行它们的“描述符魔法”(先打电话给他们的__get__())。

【讨论】:

以上是关于在python中嵌套描述符/装饰器的主要内容,如果未能解决你的问题,请参考以下文章

32.Python面向对象描述符运算符底层装饰器:闭包-闭包参数-内置装饰器-类装饰器

32.Python面向对象描述符运算符底层装饰器:闭包-闭包参数-内置装饰器-类装饰器

Python之路:描述符,类装饰器,元类

32.Python面向对象描述符运算符底层装饰器:闭包-闭包参数-内置装饰器-类装饰器

5.初识python装饰器 高阶函数+闭包+函数嵌套=装饰器

python装饰器,嵌套函数[重复]