Clearable, per-instance method cache in Python

In standard Python, you can do this to cache the return value of an expensive function:

@lru_cache()
def function(n):
    ...

This works well, but it does not work very well for class methods:

class MyClass:

    @lru_cache()
    def function(self):
        ...

All instances of MyClass will share the same cache. Often, especially for immutable instances, a per-instance cache of size 1 is desired. Python 3.8 adds a useful cached_property decorator, but that does not provide a cache_clear method like lru_cache does.

So how do you create a per-instance cache for class methods with a clear function? This is what I came up with:

class _CacheCallable:
    MISSING = object()

    def __init__(self, cache_key: str, method: types.FunctionType,
                 instance: Any):
        self.method = method
        self.instance = instance
        self.cache = instance.__dict__
        self.cache_key = cache_key

    def __call__(self) -> Any:
        cached = self.cache.get(self.cache_key, self.MISSING)
        if cached is self.MISSING:
            cached = self.method(self.instance)
            self.cache[self.cache_key] = cached
        return cached

    def cache_clear(self):
        self.cache[self.cache_key] = self.MISSING


class cache:
    def __init__(self, method: types.FunctionType):
        self.method = method
        self.cache_key = f"_cache_decorator_{method.__name__}"

    def __get__(self, instance: Any, _):
        assert instance, "Method must be called on an object."
        return _CacheCallable(self.cache_key, self.method, instance)

Note that this code is missing locks for threading (just look at the implementation of cached_property  on how to add that). It also does not handle any function arguments (also relatively easy to add).

Usage:

class C:
    def __init__(self, i):
        self.i = i

    @cache
    def f(self):
        print(f"C.f() called.")
        return self.i

c1 = C(1)
c2 = C(2)

print(c1.f(), c2.f())
print(c1.f(), c2.f())

Output:

C.f() called.
C.f() called.
1 2
1 2
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s