There must be a better way to do class-attributes -- allow descriptor.__set__ on `type`, not just `object`

Over the past few weeks/months I have had on an off issues with the way python handles class-attributes. Consider the default way to do it: ``` class A: attr = 2 """Docstring of the attribute""" ``` There are 2 main issues I have with this way of setting class-attributes: 1. It does not allow to set class-attributes that depend on other class-attributes. (Example: setting a path that depends on cls.__name__) 2. It does not allow to set lazily evaluated attributes. If setting the class-attribute involves an expensive computation, it will be done at class definition time. One possible resolution is to use metaclasses, however this comes with its own issues: 1. MetaClasses do not work nice with documentation. Attributes defined my metaclasses will neither show up in help(), dir(), nor will they be in the correct place when documenting the class with tools like sphinx. 2. Who wants to write an additional metaclass whenever they want to write a class and have some minimal class-attribute processing? A new possible resolution, possible since python 3.9, is the combination of @classmethod and @property. However, this has it's own problems and pitfalls, cf. https://bugs.python.org/issue44904, https://bugs.python.org/issue45356. In fact, after lots of trial and error and reading into descriptors I figured that "true" class-properties are not possible in the current versions of python for a very simple reason: ╔══════════════════════════════════════════════════════╗ ║descriptor's __set__ method is never called when setting a class-attribute. (see MWE) ║ ╚══════════════════════════════════════════════════════╝ Therefore, I propose the following: 1. By default `type.__setattr__ ` should check whether the attribute implements `__set__` and if so, use that instead. (it already works like this for object.__setattr__), (cf. PatchedTrig below) 2. There should be a clearer distinction between class-attributes and instances-attributes. Instances should probably by default not be allowed to change class-attributes, since this can affect global state. 3. It would be very nice to have a built-in @attribute decorator, much like @property, but specifically for stetting class attributes. (again, metaclasses are possible but the issues with documentation compatibility seem very bad) (pretty much what @classmethod@property promises but doesn't fulfill.) ## MWE ```python import math class TrigConst: def __init__(self, const=math.pi): self.const = const def __get__(self, obj, objtype=None): return self.const def __set__(self, obj, value): print(f"__set__ called", f"{obj=}") self.const = value class Trig: const = TrigConst() ``` Then ```python Trig().const # calls TrigConst.__get__ Trig().const = math.tau # calls TrigConst.__set__ Trig.const # calls TrigConst.__get__ Trig.const = math.pi # overwrites TrigConst attribute with float. ``` Patched version ```python class PatchedSetattr(type): def __setattr__(cls, key, value): if hasattr(cls, key): obj = cls.__dict__[key] if hasattr(obj, "__set__"): obj.__set__(cls, value) else: super().__setattr__(key, value) class PatchedTrig(metaclass=PatchedSetattr): const = TrigConst() ``` Then ```python cls = Trig print(cls.const, type(cls.__dict__["const"])) cls.const = math.tau print(cls.const, type(cls.__dict__["const"])) ``` gives ``` 3.141592653589793 <class '__main__.TrigConst'> 6.283185307179586 <class 'float'> ``` But ```cls cls = PatchedTrig print(cls.const, type(cls.__dict__["const"])) cls.const = math.tau print(cls.const, type(cls.__dict__["const"])) ``` gives ``` 3.141592653589793 <class '__main__.TrigConst'> __set__ called obj=<class '__main__.PatchedTrig'> 6.283185307179586 <class '__main__.TrigConst'> ```
participants (1)
-
Randolf Scholz