50

Using the LRU Cache decorator found here: http://code.activestate.com/recipes/578078-py26-and-py30-backport-of-python-33s-lru-cache/

from lru_cache import lru_cache
class Test:
    @lru_cache(maxsize=16)
    def cached_method(self, x):
         return x + 5

I can create a decorated class method with this but it ends up creating a global cache that applies to all instances of class Test. However, my intent was to create a per instance cache. So if I were to instantiate 3 Tests, I would have 3 LRU caches rather than 1 LRU cache that for all 3 instances.

The only indication I have that this is happening is when calling the cache_info() on the different class instances decorated methods, they all return the same cache statistics (which is extremely unlikely to occur given they are being interacted with very different arguments):

CacheInfo(hits=8379, misses=759, maxsize=128, currsize=128)
CacheInfo(hits=8379, misses=759, maxsize=128, currsize=128)
CacheInfo(hits=8379, misses=759, maxsize=128, currsize=128)

Is there a decorator or trick that would allow me to easily cause this decorator to create a cache for each class instance?

crzysdrs
  • 503
  • 1
  • 4
  • 4
  • 2
    Remember, a decorator is just syntax sugar for `def method: pass; method = decorate(method)`. So you can mechanically translate this to create the decorated method in your `__init__`. – Francis Avila Feb 18 '13 at 22:41
  • Are you sure that you know what a "class method" is for? Because I think you look for a normal method. If a class method is instance specific, it is, by definition, a normal method of an instance. Or why exactly do you even need a class method? Or why do you want a "per-instance" cache? – Mayou36 Nov 23 '17 at 08:58

3 Answers3

59

Assuming you don't want to modify the code (e.g., because you want to be able to just port to 3.3 and use the stdlib functools.lru_cache, or use functools32 out of PyPI instead of copying and pasting a recipe into your code), there's one obvious solution: Create a new decorated instance method with each instance.

class Test:
    def cached_method(self, x):
         return x + 5
    def __init__(self):
         self.cached_method = lru_cache(maxsize=16)(self.cached_method)
abarnert
  • 354,177
  • 51
  • 601
  • 671
  • 2
    Awesome. But how can I do this for a get-only @property? – Gilly Nov 10 '16 at 02:11
  • @Gilly you don't, you just make a _variable attribute and store the cached values there. – RBF06 Mar 27 '19 at 13:56
  • @abarnert in your example `cached_method` is a variable and a method name as well, in constructor variable the variable `self.cached_method` stores the value of `cached_method` method ? – Ciasto piekarz Feb 25 '20 at 17:53
  • 1
    One subtle point is that this creates a reference cycle of `self` -> `lru_cache` instance -> `self`. This is usually okay, but worth being aware of, because it shifts it from being cleaned up by simple reference counting to it being cleaned up by eventual and potentially disabled garbage collection. – mtraceur Mar 09 '20 at 19:58
  • 1
    **Note:** for using this answer with magic methods (like `__call__`) see [this](https://stackoverflow.com/questions/53423121/why-does-functools-lru-cache-not-cache-call-while-working-on-normal-methods). In short for using with `__call__` method the answer will need to be changed to: `self.__class__.__call__ = lru_cache(maxsize=16)(self.__class__.__call__)` – vladkha Apr 04 '20 at 22:07
5

How about this: a function decorator that wraps the method with lru_cache the first time it's called on each instance?

def instance_method_lru_cache(*cache_args, **cache_kwargs):
    def cache_decorator(func):
        @wraps(func)
        def cache_factory(self, *args, **kwargs):
            print('creating cache')
            instance_cache = lru_cache(*cache_args, **cache_kwargs)(func)
            instance_cache = instance_cache.__get__(self, self.__class__)
            setattr(self, func.__name__, instance_cache)
            return instance_cache(*args, **kwargs)
        return cache_factory
    return cache_decorator

Use it like this:

class Foo:
    @instance_method_lru_cache()
    def times_2(self, bar):
        return bar * 2

foo1 = Foo()
foo2 = Foo()

print(foo1.times_2(2))
# creating cache
# 4
foo1.times_2(2)
# 4

print(foo2.times_2(2))
# creating cache
# 4
foo2.times_2(2)
# 4

Here's a gist on GitHub with some inline documentation.

z0r
  • 8,185
  • 4
  • 64
  • 83
2

These days, methodtools will work

from methodtools import lru_cache
class Test:
    @lru_cache(maxsize=16)
    def cached_method(self, x):
         return x + 5

You need to install methodtools

pip install methodtools

If you are still using py2, then functools32 also is required

pip install functools32
youknowone
  • 919
  • 6
  • 14