Skip to content

Instantly share code, notes, and snippets.

@mara004
Last active July 23, 2026 12:57
Show Gist options
  • Select an option

  • Save mara004/f2926b1fcc8847a69e0af6f4e33934fd to your computer and use it in GitHub Desktop.

Select an option

Save mara004/f2926b1fcc8847a69e0af6f4e33934fd to your computer and use it in GitHub Desktop.
Custom cached property implementation (backward compatible)
# 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
@mara004

mara004 commented Jul 20, 2026

Copy link
Copy Markdown
Author

For the record:

>>> import functools
>>> def cached_property(func):
...     return property( functools.lru_cache(maxsize=1)(func) )
... 
>>> class Test:
...     @cached_property
...     def value(self):
...             print("Hi")
...             return 10
... 
>>> t1 = Test()
>>> t2 = Test()
>>> t1.value
Hi
10
>>> t1.value
10
>>> t2.value
Hi
10
>>> t1.value
Hi
10

@mara004

mara004 commented Jul 20, 2026

Copy link
Copy Markdown
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
Hi
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 20, in __get__
AttributeError: 'Test' object attribute 'value' is read-only

@mara004

mara004 commented Jul 21, 2026

Copy link
Copy Markdown
Author

https://gist.github.com/mara004/076e3110aba8e8a3f8a6f2c84af350ee includes handling of cached properties on slotted classes, abstracted through a metaclass.

@mara004

mara004 commented Jul 23, 2026

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment