Mechanics of python attribute setting on class types, for use in descriptor setters
Asked Answered
S

1

0

Recently, with the change of the @classmethod decorator no longer being able to wrap the @property decorator (Python >= 3.11), there has been significant interest in how to create a @classproperty decorator. The discussion thus far only talks about getter methods, rather than setters.

I want to create a descriptor decorator @classproperty that operates the same on both the class type as well as a class instance. For example:

class classproperty:
    def __init__(self, fget=None, fset=None):
        self.fget = fget
        self.fset = fset
        super().__init__()
        
    def __get__(self, instance, owner):
        return self.fget(owner)
    
    def __set__(self, instance, value):
        owner = type(instance)
        self.fset(owner, value)
        
    def setter(self, fset):
        self.fset = fset
        return self

class A():
    _x = 0

    @classproperty
    def x(cls):
        return cls._x

    @x.setter
    def x(cls, value):
        cls._x = value

I'm stuck that whenever I set the classproperty through the class (i.e. A.x=1) instead of the object (i.e. A().x = 1), the method is overriden rather than calling classproperty.__set__(). For example:

>>> a = A()
>>> a.x     # Calls A.__getattribute__, A.x.__get__
0
>>> a.x = 1 # Calls A.__setattr__, A.x.__set__
>>> a.x     # Calls A.__getattribute__, A.x.__get__ 
1
>>> a._x    # Calls A.__getattribute__
1

and

>>> a = A
>>> a.x     # Calls A.x.__get__
0
>>> a.x = 1 # Doesn't call __set__, __setattr__, __setattribute__ ## last one fictitious?
>>> a.x     # Doesn't call __get__, __getattr__, __getattribute__
1
>>> a._x    # Doesn't call __get__, __getattr__, __getattribute__
0

It seems this stems from the implementation of dotted lookups on instances versus classes. This documentation mostly deals with __get__ methods, rather than __set__. This leads me to my main question - how does the dotted lookup and assignment work for class types? Is there any way to make the class type to recognize descriptor setter before overriding the class attribute?

I suspect there's something useful in the pure python equivalents for properties and class methods that might provide a path forwards, but I haven't been able to figure it out despite many hours of tinkering.

Styracaceous answered 2/4, 2024 at 7:23 Comment(0)
M
1

Unlike __get__, which is called for descriptor accesses from both an instance and a class, __set__ is called only on a descriptor assignment to an instance, but not to a class. This ensures that overwriting a descriptor attribute of a class with another value can work.

Since a class is really an instance of a metaclass, a settable class property is therefore possible by defining it in the metaclass of a class, as already pointed out in this answer.

But if you also want to make the class property work when accessed via an instance, you can add a condition in the getter and setter methods to normalize access from an instance to the class of the instance, and make the descriptor defined in the metaclass available on the class:

class MetaA(type):
    _x = 0

    @property
    def x(cls):
        if not isinstance(cls, type):
            cls = type(cls)
        return cls._x

    @x.setter
    def x(cls, value):
        if not isinstance(cls, type):
            cls = type(cls)
        cls._x = value

class A(metaclass=MetaA):
    x = MetaA.x

so that:

print(A.x)
A.x = 1
a = A()
print(a.x)
a.x = 2
print(A.x)

outputs:

0
1
2

Demo: https://ideone.com/YfWTac

Mendymene answered 2/4, 2024 at 9:5 Comment(1)
I had seen this approach also in the first thread I linked - thanks for making it explicit! I think I was hoping for a solution without metaclasses, but this is very clear.Styracaceous

© 2022 - 2025 — McMap. All rights reserved.