I have a simple little decorator, that caches results from function calls in a dict
as a function attribute.
from decorator import decorator
def _dynamic_programming(f, *args, **kwargs):
try:
f.cache[args]
except KeyError:
f.cache[args] = f(*args, **kwargs)
return f.cache[args]
def dynamic_programming(f):
f.cache = {}
return decorator(_dynamic_programming, f)
I now want to add the possibility to empty the cache. So I change the dynamic_programming()
function like this:
def dynamic_programming(f):
f.cache = {}
def clear():
f.cache = {}
f.clear = clear
return decorator(_dynamic_programming, f)
Now let's assume I use this little thing to implement a Fibonacci number function:
@dynamic_programming
def fib(n):
if n <= 1:
return 1
else:
return fib(n-1) + fib(n-2)
>>> fib(4)
5
>>> fib.cache
{(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5}
But now when I clear the cache something weird happens:
>>> fib.clear()
>>> fib.cache
{(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5}
Or (with a new Python kernel running) do it the other way round:
>>> fib.clear()
>>> fib(4)
5
>>> fib.cache
{}
Why is the cache somehow not 'reachable' after the first access to it, i.e. not changing when calling clear()
after a call or a call after a clear()
?
(Btw. I know a solution to clear the cache correctly: calling f.cache.clear()
instead of assigning {}
to it works as expected. I'm merely interested in the reason why the assigning solution fails.)
The problem is with the decorator
module. If you add some print
statements to your decorator:
from decorator import decorator
def _dynamic_programming(f, *args, **kwargs):
print "Inside decorator", id(f.cache)
try:
f.cache[args]
except KeyError:
f.cache[args] = f(*args, **kwargs)
return f.cache[args]
def dynamic_programming(f):
f.cache = {}
print "Original cache", id(f.cache)
def clear():
f.cache = {}
print "New cache", id(f.cache)
f.clear = clear
return decorator(_dynamic_programming, f)
@dynamic_programming
def fib(n):
if n <= 1:
return 1
else:
return fib(n-1) + fib(n-2)
print fib(4)
print id(fib.cache)
fib.clear()
print id(fib.cache)
print fib(10)
print id(fib.cache)
It outputs (duplicate lines skipped):
Original cache 139877501744024
Inside decorator 139877501744024
5
139877501744024
New cache 139877501802208
139877501744024
Inside decorator 139877501802208
89
139877501744024
As you can see, the cache
inside the decorator changes according the the clear function. However, the cache
accessed from __main__
does not change. Printing the cache
outside and inside the decorator give a clearer picture (again, duplicates skipped):
Inside decorator {}
Inside decorator {(1,): 1}
Inside decorator {(2,): 2, (0,): 1, (1,): 1}
Inside decorator {(2,): 2, (0,): 1, (3,): 3, (1,): 1}
5
Outside {(2,): 2, (0,): 1, (3,): 3, (1,): 1, (4,): 5}
Inside decorator {}
Inside decorator {(1,): 1}
Inside decorator {(2,): 2, (0,): 1, (1,): 1}
Inside decorator {(2,): 2, (0,): 1, (3,): 3, (1,): 1}
Inside decorator {(2,): 2, (0,): 1, (3,): 3, (1,): 1, (4,): 5}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5, (5,): 8}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5, (5,): 8, (6,): 13}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5, (5,): 8, (6,): 13, (7,): 21}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (8,): 34, (3,): 3, (4,): 5, (5,): 8, (6,): 13, (7,): 21}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (8,): 34, (3,): 3, (9,): 55, (4,): 5, (5,): 8, (6,): 13, (7,): 21}
89
Outside {(2,): 2, (0,): 1, (3,): 3, (1,): 1, (4,): 5}
As you can see, the inside changes are not echoed on the outside. The problem is that inside the decorator
module, there is the line (inside the class it used to make the decorator):
self.dict = func.__dict__.copy()
And then later:
func.__dict__ = getattr(self, 'dict', {})
So basically, the __dict__
on the outside is different from the __dict__
on the inside. This means that:
__dict__
is copied (not referenced) by the decoratorcache
changes, it changes the inside __dict__
, not the outside __dict__
cache
used by the _dynamic_programming
is cleared, but you cannot see that from the outside, as the decorator's __dict__
is still pointing to the old cache
(as you can see above, as the inside cache
updates, while the outside cache
remains the same)So, to summarise, it's a problem with the decorator
module.