混合抽象方法、类方法和 属性 装饰器时的奇怪行为
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
。
我一直在尝试看看是否可以通过混合三个装饰器(在 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
。