msgspec
msgspec copied to clipboard
Automatically support `functools.cached_property` without requiring setting `dict=True`
Description
Since msgspec.Struct types are effectively slotted classes, using functools.cached_property with structs requires the user to also set dict=True:
import functools
import msgspec
class RightTriangle(msgspec.Struct, dict=True): # <- dict=True required for cached_property
a: float
b: float
@functools.cached_property
def c(self):
return (self.a ** 2 + self.b ** 2) ** 0.5
This is because the cpython implementation of cached_property requires a __dict__ exist on the instance. The error raised if you fail to set dict=True happens on property access, and since it's from functools not msgspec it doesn't indicate how to resolve the issue:
TypeError: No '__dict__' attribute on 'RightTriangle' instance to cache 'c' property.
As such, users have to read the docs (or ask) to know how to mix msgspec.Struct and functools.cached_property together. It would be nice to improve this situation. A few options:
- Automatically set
dict=Trueif the class includes anycached_propertyattributes. This lets things "just work" for most users. - Warn if a class includes
cached_propertyattributes but doesn't setdict=True. This is like a less automagic form of 1. - Rewrite
cached_propertyattributes onmsgspec.Structtypes ifdict=Falseto use an alternative slots-based mechanism, similar to whatattrsadded here.
Of these options I'm leaning towards 1 or maybe 3.
Upon further investigation, I'm now leaning more towards 3.
- Some of the preprocessing we'll need to do to make this work will also be needed to support #573.
- Providing builtin support for this will avoid the need to allocate a
__dict__per instance, reducing memory usage somewhat and slightly accelerating attribute access - Likewise, handling this ourselves lets us backport the 3.12 fix to avoid usage of an
RLockper cached property, which can provide unnecessary lock contention in threaded applications.
The main downsides are:
- More code on our end
- Some magic. When used on a
Structtype, thecached_propertyimplementation itself won't ever actually run, it's just a decorator flag to tell theStructimplementation to treat it as a cached property using our own implementation. Sinceattrsis already doing the same, I feel better following their lead.
- Some magic. When used on a
Structtype, thecached_propertyimplementation itself won't ever actually run, it's just a decorator flag to tell theStructimplementation to treat it as a cached property using our own implementation. Sinceattrsis already doing the same, I feel better following their lead.
What about not using functools.cached_property, but instead provide e.g. msgspec.cached_property, which would work the same way you described, but would make it more explicit that this is not the same as the functools one? That would eliminate the magic aspect of it while providing the same functionality.