Solution
Below a small drop-in replacement for (and wrapper around) lru_cache
which puts the LRU cache on the instance (object) and not on the class.
Summary
The replacement combines lru_cache
with cached_property
. It uses cached_property
to store the cached method on the instance on first access; this way the lru_cache
follows the object and as a bonus it can be used on unhashable objects like a non-frozen dataclass
.
How to use it
Use @instance_lru_cache
instead of @lru_cache
to do decorate a method and you're all set. Decorator arguments are supported, e.g. @instance_lru_cache(maxsize=None)
Comparison with other answers
The result is comparable to the answers provided by pabloi and akaihola, but with a simple decorator syntax. Compared to the answer provided by youknowone, this decorator is type hinted and does not require third-party libraries (result is comparable).
This answer differs from the answer provided by Raymond Hettinger as the cache is now stored on the instance (which means the maxsize is defined per instance and not per class) and it works on methods of unhashable objects.
from functools import cached_property, lru_cache, partial, update_wrapper
from typing import Callable, Optional, TypeVar, Union
T = TypeVar("T")
def instance_lru_cache(
method: Optional[Callable[..., T]] = None,
*,
maxsize: Optional[int] = 128,
typed: bool = False
) -> Union[Callable[..., T], Callable[[Callable[..., T]], Callable[..., T]]]:
"""Least-recently-used cache decorator for instance methods.
The cache follows the lifetime of an object (it is stored on the object,
not on the class) and can be used on unhashable objects. Wrapper around
functools.lru_cache.
If *maxsize* is set to None, the LRU features are disabled and the cache
can grow without bound.
If *typed* is True, arguments of different types will be cached separately.
For example, f(3.0) and f(3) will be treated as distinct calls with
distinct results.
Arguments to the cached method (other than 'self') must be hashable.
View the cache statistics named tuple (hits, misses, maxsize, currsize)
with f.cache_info(). Clear the cache and statistics with f.cache_clear().
Access the underlying function with f.__wrapped__.
"""
def decorator(wrapped: Callable[..., T]) -> Callable[..., T]:
def wrapper(self: object) -> Callable[..., T]:
return lru_cache(maxsize=maxsize, typed=typed)(
update_wrapper(partial(wrapped, self), wrapped)
)
return cached_property(wrapper) # type: ignore
return decorator if method is None else decorator(method)