【问题标题】:Strange behaviour when mixing abstractmethod, classmethod and property decorators混合抽象方法、类方法和属性装饰器时的奇怪行为
【发布时间】:2021-10-16 04:12:09
【问题描述】:

我一直在尝试查看是否可以通过混合三个装饰器来创建抽象类属性(在 Python 3.9.6 中,如果这很重要的话),我注意到一些奇怪的行为。

考虑以下代码:

from abc import ABC, abstractmethod

class Foo(ABC):
    @classmethod
    @property
    @abstractmethod
    def x(cls):
        print(cls)
        return None

class Bar(Foo):
    @classmethod
    @property
    def x(cls):
        print("this is executed")
        return super().x

这个输出

this is executed
<class '__main__.Bar'>

这意味着不知何故,Bar.x 最终被调用了。

PyCharm 警告我 Property 'self' cannot be deleted。如果我颠倒@classmethod 和@property 的顺序,则不会调用Bar.x,但我仍然会收到相同的警告,还有另一个警告:This decorator will not receive a callable it may expect; the built-in decorator returns a special object(每当我将@property 放在@987654331 上方时,也会出现这种情况@)。

删除三个装饰器中的任何一个(进行适当的更改:在删除 @property 时添加 () 或在删除 @classmethod 时将 cls 更改为 self)也会阻止调用 Bar.x。

我想所有这一切都意味着直接混合这些装饰器可能只是一个坏主意(正如此处关于其他线程中的类属性的讨论所表明的那样)。

不过,我很好奇:这里发生了什么?为什么要调用 Bar.x?

【问题讨论】:

  • 当你说“类属性”时,你想达到什么目的?您可能不应该尝试混合使用property 和classmethod,而是创建您自己的自定义描述符。那会更理智。
  • @juanpa.arrivillaga:现在实际上支持混合classmethod 和property - 他们改变了classmethod 与任意描述符的交互方式。
  • @user2357112supportsMonica 啊,是的。我实际上记得在新的和改进的 Descriptor HOWTO 中看到了这一点,基本上它检查函数是否有 __get__ 并使用类调用它。
  • @user2357112supportsMonica:没有@abstractmethod 也能正常工作的事实让我觉得是这样!您还记得此功能/更改记录在哪里吗?我想看看细节。
  • @tomasz:我不确定是否有任何详细的文档。我只有classmethod docs 中的这条注释:“在 3.9 版中更改:类方法现在可以包装其他描述符,例如 property()。”

标签: python abstract-class python-decorators


【解决方案1】:

这看起来像是检查继承的抽象方法的逻辑中的错误。

如果检索其__isabstractmethod__ 属性产生True,则将类dict 中的对象视为抽象对象。当Bar子类Foo时,Python需要判断Bar是否覆盖了抽象Foo.x,如果是,覆盖本身是否抽象。它应该通过在 MRO 中搜索类字典中的 'x' 条目来执行此操作,因此它可以直接检查描述符上的 __isabstractmethod__ 而无需调用描述符协议,而是执行 a simple Bar.x attribute access .

Bar.x 属性访问调用类属性。它还返回 None 而不是 abstract 属性,并且 None 不是抽象的,因此 Python 对 Bar.x 是否是抽象的感到困惑。由于检查不同,Python 最终仍然认为 Bar.x 是抽象的,但是如果您稍微更改一下示例:

>>> from abc import ABC, abstractmethod
>>> 
>>> class Foo(ABC):
...     @classmethod
...     @property
...     @abstractmethod
...     def x(cls):
...         print(cls)
...         return None
... 
>>> class Bar(Foo): pass
... 
<class '__main__.Bar'>
>>> Bar()
<__main__.Bar object at 0x7f46eca8ab80>

Python 最终认为 Bar 是一个具体的类,即使更改后的示例根本没有覆盖 x。

【讨论】:

  • 这很清楚,内容丰富,谢谢!
  • 我基于这个线程提交了一个错误报告:bugs.python.org/issue44904
  • @tomasz:抱歉,我误读了原始问题中的代码。是的,这是一个错误!和你一样。
  • (具体来说,我没有注意到在发生子类化时该属性被“调用”,尽管从未创建过该类的实例。)
  • @tomasz: ...是的,我应该说“尽管 Bar.x 从未在类或类的实例上被调用过”。 (我发现在一个类上调用 classmethod property 的想法,而不是类的实例,有点令人费解,我不得不承认。)
【解决方案2】:

您可以尝试在Bar.x 中引发异常。这样你就可以看到它在哪里被调用了。

它应该引导你到标准库中的abc.py,特别是_abc_init(cls)这一行。这个函数是用C实现的。它做的第一件事就是调用compute_abstract_methods(self)检查类继承的所有抽象方法,看看它们是否被实现。这意味着获取Bar.x,它会调用属性getter。

【讨论】:

    猜你喜欢
    • 2021-09-17
    • 2021-01-26
    • 2013-10-20
    • 1970-01-01
    • 2016-10-28
    • 2014-06-25
    • 2018-12-13
    • 2022-12-31
    • 2021-10-22
    相关资源
    最近更新 更多