Thank you, that’s exactly the main counterargument for leaving cached_property as is. I also wrote my own descriptor for a thread-local cached property, although I did subclasss cached_property to inherit __init__ and __set_name__ and avoid copying 12 lines. I could save a few more lines if there were _get_cached/_set_cached/_del_cached hooks to override but as you point out the whole class is ~40 lines so at the end of the day it’s not a huge deal for a relatively rare need.
Werkzeug also has a relatively simple variation of cached_property that handles both __dict__ and __slots__. In the case of slotted classes, it expects a _cache_{property name} attribute to exist.
Or you can do it the old fashioned way:
#!/usr/bin/env python3
class Matrix:
__slots__ = ("_the_one")
def __init__(self):
self._the_one = None
@property
def the_one(self)->str:
if self._the_one is None:
self._the_one = "Neo"
return self._the_one
def take_blue_pill(self)->None:
self._the_one = None
the_matrix = Matrix()
print(the_matrix.the_one)
the_matrix.take_blue_pill()
print(the_matrix._the_one)
This is interesting.
Previously I’ve just had my dataclass-like slotting tool add __dict__ in the case where it found a @cached_property in order to just make them work. For whatever reason I didn’t think you could replace the slot attribute in a class.
It seems like wrapping the function and slot together can make for a fairly simple descriptor that can be used to replace a cached_property on the class[1]. The requirement being that it’s linked with a metaclass or class-rebuilder[2] that can remove the original object so the slot can be placed before replacing the slot descriptor with the wrapper. For dataclasses to support something like this the logic would probably need to be in the _add_slots function.
Yes, indeed it can be done with a class rebuilding decorator like @dataclass(slots=True) as well.
Here’s a proof of concept:
def slotify(cls):
slots = {}
others = {}
for name, obj in vars(cls).items():
(slots if getattr(obj, '_is_slot', False) else others)[name] = obj
cls = type(cls)(cls.__name__, cls.__bases__,
{
**others,
'__slots__': (*getattr(cls, '__slots__', ()), *slots)
}
)
for name, func in slots.items():
slot = getattr(cls, name)
def fallback(self, getter=slot.__get__, name=name, func=func):
try:
return getter(self)
except AttributeError:
setattr(self, name, value := func(self))
return value
setattr(cls, name, property(fallback, slot.__set__, slot.__delete__))
return cls
def slot_property(func):
func._is_slot = True
return func
so that:
@slotify
class Foo:
@slot_property
def a(self):
print('called')
return 1
f = Foo()
print(f.a) # called, 1
print(f.a) # 1
f.a = 2
print(f.a) # 2
del f.a
print(f.a) # called, 1
I’ve gone with a descriptor that looks more like this rather than creating a fallback @property so that it looks roughly the same as the regular cached_property except it also wraps the slot:
class _SlottedCachedProperty:
def __init__(self, slot, func):
self.slot = slot
self.func = func
self.__doc__ = self.func.__doc__
self.__module__ = self.func.__module__
def __get__(self, instance, owner=None):
if instance is None:
return self
try:
return self.slot.__get__(instance, owner)
except AttributeError:
pass
result = self.func(instance)
self.slot.__set__(instance, result)
return result
def __set__(self, obj, value):
self.slot.__set__(obj, value)
def __delete__(self, obj):
self.slot.__delete__(obj)
I have a PR for it here on my own dataclass-like tool.
I don’t really like the decorator rebuilding a class that dataclasses and attrs do as there’s a lot of extra cleaning up you have to do to make things like no arguments super() work and there are still ways the original unslotted class can be kept around unintentionally.
I do think this might be a cleaner approach than the __getattr__ rewriting attrs is doing.