混合抽象方法、类方法和 属性 装饰器时的奇怪行为

Strange behaviour when mixing abstractmethod, classmethod and property decorators

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

考虑以下代码:

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 放在 @classmethod 上面)。

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

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

不过,我很好奇:这是怎么回事?为什么叫Bar.x?

您可以尝试在 Bar.x 中引发异常。这样就可以看到调用的地方了

它应该会引导您进入标准库中的 abc.py,特别是行 _abc_init(cls). This function is implemented in C. One of the first things this does is call compute_abstract_methods(self) 检查 class 继承的所有抽象方法以查看它们是否已实现。这意味着获取 Bar.x 调用 属性 getter.

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

如果检索 __isabstractmethod__ 属性产生 True,则 class 字典中的对象被认为是抽象的。 Bar subclasses Foo时,Python需要判断Bar是否覆盖抽象Foo.x,如果是,是否覆盖本身抽象。它 应该 通过在 class 字典中搜索 'x' 条目的 MRO 来做到这一点,因此它可以直接检查描述符上的 __isabstractmethod__ 而无需调用描述符协议,而是执行 a simple Bar.x attribute access.

Bar.x 属性访问调用 class 属性。它也是 returns None 而不是抽象 属性,并且 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 是一个具体的 class,即使更改后的示例根本没有覆盖 x