混合抽象方法、类方法和属性装饰器时的奇怪行为
我一直在尝试查看是否可以通过混合三个装饰器来创建抽象类属性(在 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。如果我颠倒@classmethodand的顺序@property,Bar.x则不会调用,但我仍然收到相同的警告,还有另一个警告:(This decorator will not receive a callable it may expect; the built-in decorator returns a special object每当我放在@property上面时也会出现@classmethod)。
删除三个装饰器中的任何一个(进行适当的更改:()在删除时添加@property或在删除时更改cls为)也可以防止被调用。self@classmethodBar.x
我想所有这些都意味着直接混合这些装饰器可能只是一个坏主意(如此处其他线程中关于类属性的讨论所示)。
尽管如此,我很好奇:这里发生了什么?为什么叫 Bar.x?
回答
这看起来像是检查继承的抽象方法的逻辑中的错误。
如果检索其__isabstractmethod__属性产生 ,则类 dict 中的对象被认为是抽象的True。当创建Bar子类时Foo,Python 需要判断是否Bar覆盖了抽象Foo.x,如果覆盖了,覆盖本身是否是抽象的。它应该通过在 MRO 中搜索'x'类 dict 中的条目来完成此操作,因此它可以__isabstractmethod__直接检查描述符而无需调用描述符协议,而是执行简单的Bar.x属性访问。
该Bar.x属性访问调用类的属性。它还返回None而不是抽象属性,并且None不是抽象的,因此 Python 对是否Bar.x是抽象感到困惑。Bar.x由于不同的检查,Python 最终仍然认为是抽象的,但是如果您稍微更改示例:
>>> 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。