Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strange behaviour when mixing abstractmethod, classmethod and property decorators

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?

like image 330
tomasz Avatar asked Aug 12 '21 19:08

tomasz


People also ask

What does Abstractmethod decorator do?

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.

Which decorator is used to create an abstract method?

To define an abstract method we use the @abstractmethod decorator of the abc module.


2 Answers

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.

like image 182
user2357112 supports Monica Avatar answered Oct 06 '22 16:10

user2357112 supports Monica


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.

like image 43
Jasmijn Avatar answered Oct 06 '22 16:10

Jasmijn