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).
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.