Can anyone please help me understand how the cache variable still exists even after the _cachedf function is returned?
It has to do with Python's reference counting garbage collector. The cache
variable will be conserved and accessible since the function _cachedf
has a reference to it, and the caller to cached
has a reference to that. When you call the function again, you are still using the same function object that was originally created, hence you still have access to the cache.
You won't lose the cache until all references to it are destroyed. You can use the del
operator to do that.
For example:
>>> import time
>>> def cached(f):
... cache = {} # <---- not an attribute this time!
... def _cachedf(*args):
... if args not in cache:
... cache[args] = f(*args)
... return cache[args]
... return _cachedf
...
...
>>> def foo(duration):
... time.sleep(duration)
... return True
...
...
>>> bob = cached(foo)
>>> bob(2) # Takes two seconds
True
>>> bob(2) # returns instantly
True
>>> del bob # Deletes reference to bob (aka _cachedf) which holds ref to cache
>>> bob = cached(foo)
>>> bob(2) # takes two seconds
True
>>>
For the record, what you're trying to acheive is called Memoization, and there is a more complete memoizing decorator available from the decorator pattern page which does the same thing, but using a decorator class. Your code and the class-based decorator are essentially the same, with the class-based decorator checking for hash-ability before storing.
Edit (2017-02-02): @SiminJie comments that cached(foo)(2)
always incurs a delay.
This is because cached(foo)
returns a new function with a fresh cache. When cached(foo)(2)
is called, a new fresh (empty) cache is created and then the cached function is immediately called.
Since the cache is empty and won't find the value, it re-runs the underlying function. Instead, do cached_foo = cached(foo)
and then call cached_foo(2)
multiple times. This will only incur the delay for the first call. Also, if used as a decorator, it will work as expected:
@cached
def my_long_function(arg1, arg2):
return long_operation(arg1,arg2)
my_long_function(1,2) # incurs delay
my_long_function(1,2) # doesn't
If you're not familiar with decorators, take a look at this answer to understand what the above code means.