I've been trying to see whether one can create an abstract class property by mixing the three decorators (in Python 3.9.6, if that matters), and I noticed some strange behaviour.
Consider the following code:
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 outputs
this is executed
<class '__main__.Bar'>
This means that somehow, Bar.x
ends up being called.
PyCharm warns me that Property 'self' cannot be deleted
. If I reverse the order of @classmethod
and @property
, Bar.x
is not called, but I still get the same warning, and also another one: This decorator will not receive a callable it may expect; the built-in decorator returns a special object
(this also appears whenever I put @property
above @classmethod
).
Removing any of the three decorators (with the appropriate changes: adding ()
when removing @property
or changing cls
to self
when removing @classmethod
) also prevents Bar.x
from being called.
I suppose all of this means that it's probably just a bad idea to directly mix those decorators (as indicated by discussion about class properties in other threads here).
Neverthless, I am curious: what is happening here? Why is Bar.x called?
abstractmethod(function) A decorator indicating abstract methods. Using this decorator requires that the class's metaclass is ABCMeta or is derived from it. A class that has a metaclass derived from ABCMeta cannot be instantiated unless all of its abstract methods and properties are overridden.
To define an abstract method we use the @abstractmethod decorator of the abc module.
This looks like a bug in the logic that checks for inherited abstract methods.
An object in a class dict is considered abstract if retrieving its __isabstractmethod__
attribute produces True
. When Bar
subclasses Foo
, Python needs to determine whether Bar
overrides the abstract Foo.x
, and if so, whether the override is itself abstract. It should do this by searching the MRO for an 'x'
entry in a class dict, so it can examine __isabstractmethod__
on descriptors directly without invoking the descriptor protocol, but instead, it performs a simple Bar.x
attribute access.
The Bar.x
attribute access invokes the class property. It also returns None
instead of the abstract property, and None
isn't abstract, so Python gets confused about whether Bar.x
is abstract. Python ends up still thinking Bar.x
is abstract due to a different check, but if you change the example a bit:
>>> 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 ends up thinking Bar
is a concrete class, even though the changed example doesn't override x
at all.
You can try raising an exception in Bar.x
. This way you can see where it is called.
It should lead you to abc.py
in the standard library, specifically the line _abc_init(cls)
. This function is implemented in C. One of the first things this does is call compute_abstract_methods(self)
checks all the abstract methods the class has inherited to see if they're implemented. This means getting Bar.x
which invokes the property getter.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With