Bharel
Bharel

Reputation: 26901

Using descriptors or properties with classmethod

I've seen many answers to that question, all of them saying it's impossible to use a property with a classmethod, but the following code works:

class A:
    @classmethod
    @property
    def hello(cls):
        print(1234)

>>> A.hello
1234

Why and how does it work?

Running on CPython 3.9.1.

Upvotes: 0

Views: 1293

Answers (1)

Bharel
Bharel

Reputation: 26901

Starting in Python 3.9, classmethods trigger the descriptor protocol. From the Python docs:

The code path for hasattr(obj, '__get__') was added in Python 3.9 and makes it possible for classmethod() to support chained decorators.

Surprisingly, diving a bit deeper into the subject will show you that classmethod triggers the descriptor __get__ with the class itself as the instance:

class Descriptor:
  def __get__(self, instance, owner):
    print(instance, owner)
  def __set__(self, value, owner):
    print(value, owner)

class A:
  regular = Descriptor()
  clsmethod = classmethod(Descriptor())

>>> A.regular
None <class '__main__.A'>
>>> A.clsmethod
<class '__main__.A'> None

I'm guessing they made it specifically to support descriptors such as @property, as accessing them through the class returns the property itself:

class B:
  @property
  def prop(self):
    print(self)

>>> B.__dict__["prop"].__get__(None, 1234)
<property object at 0x000001BEEB635630>
>>> B.__dict__["prop"].__get__(1234, None)
1234

It's a bit unintuitive and turns the descriptor protocol clunky if you wish to support both classmethod and normal descriptors, as you have to check if the owner is None.

Keep in mind however, __set__ is not called (as the descriptor protocol doesn't call it when setting class attributes), making you unable to use @property.setter:

>>> A.regular = 1234
>>> A.regular
1234
>>> A.clsmethod = 1234
>>> A.clsmethod
1234

Upvotes: 1

Related Questions