Last active
July 23, 2026 12:57
-
-
Save mara004/f2926b1fcc8847a69e0af6f4e33934fd to your computer and use it in GitHub Desktop.
Custom cached property implementation (backward compatible)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # SPDX-FileCopyrightText: 2026 geisserml <geisserml@gmail.com> | |
| # SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause | |
| # See also | |
| # https://github.com/pypdfium2-team/pypdfium2/blob/950aeb49b8aa1b5e9bc248b1ff293d4e586bf41b/src/pypdfium2_stl/shared/stl.py | |
| # https://stackoverflow.com/a/78825819/15547292 | |
| # https://gist.github.com/mara004/076e3110aba8e8a3f8a6f2c84af350ee | |
| class cached_property: | |
| """ | |
| Custom cached property implementation. | |
| Powered by the descriptor protocol. Zero overhead after the property has been loaded. | |
| Similar to :obj:`functools.cached_property`, but cleaner and more backward compatible. | |
| Note: | |
| Descriptor-based cached properties are inherently incompatible with ``__slots__``, as slotted classes do not allow for instance-level shadowing of class-level attributes, which however is essential for this cached property model.\n | |
| With slots, you need to take a different approach that does not work with a decorator API: Add the property names to your slots and implement a ``__getattr__`` method which dispatches the cached property functions through a map or similar, and assigns the result to the object. This is also overhead-free after load. | |
| Important: | |
| Don't be tempted to think :class:`.cached_property` could be replaced (or backported) by stacking :class:`property` and :func:`functools.lru_cache`. It cannot. | |
| :func:`~functools.lru_cache` operates on class level, not instance level, which has a ton of negative implications. | |
| In particular, when there are more instance objects than ``maxsize``, caches would be lost, whereas an unbounded class-level cache would never be cleared. | |
| Cached properties belong to instance level so that cached values remain with their objects, and eventually get garbage collected together. | |
| In general, you almost never want :func:`functools.lru_cache` except on standalone functions. It is inappropriate on methods. Unfortunately, there is no instance-level LRU cache in the standard library, but you could consider something like ``methodtools.lru_cache()``. | |
| """ | |
| def __init__(self, func): | |
| self.func = func | |
| self.assigned_name = None | |
| self.__doc__ = func.__doc__ | |
| # Optional. On older Python versions (< 3.6) that do not call this hook, the func's __name__ will be used as fallback. | |
| def __set_name__(self, cls, name): | |
| if self.assigned_name is None: | |
| self.assigned_name = name | |
| else: | |
| assert name == self.assigned_name, f"A cached property is tied to one attribute. You cannot assign to both {name!r} and {self.assigned_name!r}." | |
| def __get__(self, obj, cls=None): | |
| if obj is None: | |
| return self | |
| value = self.func(obj) | |
| name = self.assigned_name or self.func.__name__ | |
| setattr(obj, name, value) | |
| return value |
Author
Author
Also, concerning slots:
>>> class Test:
... __slots__ = ("value", )
... @cached_property
... def value(self):
... print("Hi")
... return 10
... Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: 'value' in __slots__ conflicts with class variable>>> class Test:
... __slots__ = ()
... @cached_property
... def value(self):
... print("Hi")
... return 10
...
>>> t = Test()
>>> t.value
HiTraceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 20, in __get__
AttributeError: 'Test' object attribute 'value' is read-only
Author
https://gist.github.com/mara004/076e3110aba8e8a3f8a6f2c84af350ee includes handling of cached properties on slotted classes, abstracted through a metaclass.
Author
Concerning type checking, pyright has this: https://github.com/microsoft/pyright/blob/93ea6468a40c7c8cdab5643f66411da5e0414742/packages/pyright-internal/typeshed-fallback/stdlib/functools.pyi#L248
And LLVM this: https://github.com/llvm/llvm-project/blob/cb383a37440d27238f8a01eee05228910d65d63e/clang/bindings/python/clang/cindex.py#L231-L263
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
For the record: