Welcome, guest | Sign In | My Account | Store | Cart

A simple result-caching decorator for instance methods. NOTE: does not work with plain old non-instance-method functions. The cache is stored on the instance to prevent memory leaks caused by long-term caching beyond the life of the instance (almost all other recipes I found suffer from this problem when used with instance methods).

Python, 51 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from functools import partial

class memoize(object):
    """cache the return value of a method
    
    This class is meant to be used as a decorator of methods. The return value
    from a given method invocation will be cached on the instance whose method
    was invoked. All arguments passed to a method decorated with memoize must
    be hashable.
    
    If a memoized method is invoked directly on its class the result will not
    be cached. Instead the method will be invoked like a static method:
    class Obj(object):
        @memoize
        def add_to(self, arg):
            return self + arg
    Obj.add_to(1) # not enough arguments
    Obj.add_to(1, 2) # returns 3, result is not cached
    """
    def __init__(self, func):
        self.func = func
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self.func
        return partial(self, obj)
    def __call__(self, *args, **kw):
        obj = args[0]
        try:
            cache = obj.__cache
        except AttributeError:
            cache = obj.__cache = {}
        key = (self.func, args[1:], frozenset(kw.items()))
        try:
            res = cache[key]
        except KeyError:
            res = cache[key] = self.func(*args, **kw)
        return res


if __name__ == "__main__":
    # example usage
    class Test(object):
        v = 0
        @memoize
        def inc_add(self, arg):
            self.v += 1
            return self.v + arg

    t = Test()
    assert t.inc_add(2) == t.inc_add(2)
    assert Test.inc_add(t, 2) != Test.inc_add(t, 2)

Though there are many implementations of memoize (caching) decorators on ActiveState, I have not found any that satisfactorily store the cache on the instance of an instance method. The snippet that comes closest to meeting this objective is given in a comment by Oleg Noga on this recipe. However, I find Noga's implementation lacking because it will give unexpected, indeed incorrect, results:

class memoize(object):
    def __init__(self, function):
        self._function = function
        self._cacheName = '_cache__' + function.__name__
    def __get__(self, instance, cls=None):
        self._instance = instance
        return self
    def __call__(self, *args):
        cache = self._instance.__dict__.setdefault(self._cacheName, {})
        if cache.has_key(args):
            return cache[args]
        else:
            object = cache[args] = self._function(self._instance, *args)
            return object

class A(object):
    def __init__(self, value):
        self.value = value
    @memoize
    def val(self):
        return self.value

a_val = A(1).val
b_val = A(2).val
assert a_val() != b_val(), "FAIL!"

The problem here is that __get__ saves the instance on the memoize object, and the last invocation of __get__ wins. Instead, the instance should be bound to the callable; functools.partial provides an elegant solution to that problem.