Strange behaviour when mixing abstractmethod, classmethod and property decorators

2024/9/27 9:18:55

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, abstractmethodclass Foo(ABC):@classmethod@property@abstractmethoddef x(cls):print(cls)return Noneclass Bar(Foo):@classmethod@propertydef 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?

Answer

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.

https://en.xdnf.cn/q/71465.html

Related Q&A

Center the third subplot in the middle of second row python

I have a figure consisting of 3 subplots. I would like to locate the last subplot in the middle of the second row. Currently it is located in the left bottom of the figure. How do I do this? I cannot …

Removing columns which has only nan values from a NumPy array

I have a NumPy matrix like the one below:[[182 93 107 ..., nan nan -1][182 93 107 ..., nan nan -1][182 93 110 ..., nan nan -1]..., [188 95 112 ..., nan nan -1][188 97 115 ..., nan nan -1][188 95 112 ..…

how to get kubectl configuration from azure aks with python?

I create a k8s deployment script with python, and to get the configuration from kubectl, I use the python command:from kubernetes import client, configconfig.load_kube_config()to get the azure aks conf…

Object vs. Dictionary: how to organise a data tree?

I am programming some kind of simulation with its data organised in a tree. The main object is World which holds a bunch of methods and a list of City objects. Each City object in turn has a bunch of m…

Fastest way to compute distance beetween each points in python

In my project I need to compute euclidian distance beetween each points stored in an array. The entry array is a 2D numpy array with 3 columns which are the coordinates(x,y,z) and each rows define a ne…

Calling C from Python: passing list of numpy pointers

I have a variable number of numpy arrays, which Id like to pass to a C function. I managed to pass each individual array (using <ndarray>.ctypes.data_as(c_void_p)), but the number of array may va…

Use of initialize in python multiprocessing worker pool

I was looking into the multiprocessing.Pool for workers, trying to initialize workers with some state. The pool can take a callable, initialize, but it isnt passed a reference to the initialized worker…

Pandas: select the first couple of rows in each group

I cant solve this simple problem and Im asking for help here... I have DataFrame as follows and I want to select the first two rows in each group of adf = pd.DataFrame({a:pd.Series([NewYork,NewYork,New…

Pandas: Approximate join on one column, exact match on other columns

I have two pandas dataframes I want to join/merge exactly on a number of columns (say 3) and approximately, i.e nearest neighbour, on one (date) column. I also want to return the difference (days) betw…

Adding a variable in Content disposition response file name-python/django

I am looking to add a a variable into the file name section of my below python code so that the downloaded files name will change based on a users input upon download. So instead of "Data.xlsx&quo…