diff -r bf98400daa62 Lib/functools.py --- a/Lib/functools.py Wed Dec 10 11:05:35 2014 -0500 +++ b/Lib/functools.py Thu Dec 11 14:05:17 2014 +0100 @@ -323,6 +323,9 @@ _CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"]) +class NotInCache(Exception): + pass + class _HashedSeq(list): """ This class guarantees that hash() will be called no more than once per element. This is important because the lru_cache() will hash @@ -407,13 +410,24 @@ cache = {} hits = misses = 0 full = False - cache_get = cache.get # bound method to lookup a key or return None + internal_cache_get = cache.get # bound method to lookup a key or return None lock = RLock() # because linkedlist updates aren't threadsafe root = [] # root of the circular doubly linked list root[:] = [root, root, None, None] # initialize by pointing to self if maxsize == 0: + def cache_get(args, kwds): + """Get the result for the given arguments from the cache, throw + NotInCache if the arguments are not in the cache""" + nonlocal misses + misses += 1 + raise NotInCache + + def cache_put(args, kwds, result): + """Add the argument-result pair to the cache""" + pass + def wrapper(*args, **kwds): # No caching -- just a statistics update after a successful call nonlocal misses @@ -423,11 +437,29 @@ elif maxsize is None: + def cache_get(args, kwds): + """Get the result for the given arguments from the cache, throw + NotInCache if the arguments are not in the cache""" + nonlocal hits, misses + key = make_key(args, kwds, typed) + result = internal_cache_get(key, sentinel) + if result is not sentinel: + hits += 1 + return result + else: + misses += 1 + raise NotInCache + + def cache_put(args, kwds, result): + """Add the argument-result pair to the cache""" + key = make_key(args, kwds, typed) + cache[key] = result + def wrapper(*args, **kwds): # Simple caching without ordering or size limit nonlocal hits, misses key = make_key(args, kwds, typed) - result = cache_get(key, sentinel) + result = internal_cache_get(key, sentinel) if result is not sentinel: hits += 1 return result @@ -438,13 +470,83 @@ else: + def cache_get(args, kwds): + """Get the result for the given arguments from the cache, throw + NotInCache if the arguments are not in the cache""" + nonlocal root, hits, misses + key = make_key(args, kwds, typed) + with lock: + link = internal_cache_get(key, sentinel) + if link is not sentinel: + # Move the link to the front of the circular queue + link_prev, link_next, _key, result = link + link_prev[NEXT] = link_next + link_next[PREV] = link_prev + last = root[PREV] + last[NEXT] = root[PREV] = link + link[PREV] = last + link[NEXT] = root + hits += 1 + return result + else: + misses += 1 + raise NotInCache + + def cache_put(args, kwds, result): + """Add the argument-result pair to the cache""" + nonlocal root, full + key = make_key(args, kwds, typed) + with lock: + link = internal_cache_get(key, sentinel) + if link is sentinel: + # The key is not contained yet. + if full: + # Use the old root to store the new key and result. + oldroot = root + oldroot[KEY] = key + oldroot[RESULT] = result + # Empty the oldest link and make it the new root. + # Keep a reference to the old key and old result to + # prevent their ref counts from going to zero + # during the update. That will prevent potentially + # arbitrary object clean-up code (i.e. __del__) + # from running while we're still adjusting the + # links. + root = oldroot[NEXT] + oldkey = root[KEY] + oldresult = root[RESULT] + root[KEY] = root[RESULT] = None + # Now update the cache dictionary. + del cache[oldkey] + # Save the potentially reentrant cache[key] + # assignment for last, after the root and links + # have been put in a consistent state. + cache[key] = oldroot + else: + # The cache is not full. Put the result in a new + # link at the front of the queue. + last = root[PREV] + link = [last, root, key, result] + last[NEXT] = root[PREV] = cache[key] = link + full = (len(cache) >= maxsize) + else: + # The key is in the cache already. Just move it to the + # front. + link_prev, link_next, _key, result = link + link_prev[NEXT] = link_next + link_next[PREV] = link_prev + last = root[PREV] + last[NEXT] = root[PREV] = link + link[PREV] = last + link[NEXT] = root + def wrapper(*args, **kwds): # Size limited caching that tracks accesses by recency nonlocal root, hits, misses, full key = make_key(args, kwds, typed) with lock: - link = cache_get(key) - if link is not None: + link = internal_cache_get(key, sentinel) + if link is not sentinel: # Move the link to the front of the circular queue link_prev, link_next, _key, result = link link_prev[NEXT] = link_next @@ -509,6 +611,8 @@ wrapper.cache_info = cache_info wrapper.cache_clear = cache_clear + wrapper.cache_get = cache_get + wrapper.cache_put = cache_put return update_wrapper(wrapper, user_function) return decorating_function diff -r bf98400daa62 Lib/test/test_functools.py --- a/Lib/test/test_functools.py Wed Dec 10 11:05:35 2014 -0500 +++ b/Lib/test/test_functools.py Thu Dec 11 14:05:17 2014 +0100 @@ -1077,6 +1077,30 @@ def f(): pass + def test_manual_cache_access(self): + @functools.lru_cache(maxsize=0) + def f(n): + return n + f.cache_put([5], {}, 5) + with self.assertRaises(functools.NotInCache): + f.cache_get([5], {}) + self.assertEqual(f(5), 5) + + @functools.lru_cache(maxsize=None) + def f(n): + return n + f.cache_put([4], {}, 4) + self.assertEqual(f.cache_get([4], {}), 4) + self.assertEqual(f(2), 2) + self.assertEqual(f.cache_get([2], {}), 2) + + @functools.lru_cache(maxsize=8) + def f(n): + return n + f.cache_put([4], {}, 4) + self.assertEqual(f.cache_get([4], {}), 4) + self.assertEqual(f(2), 2) + self.assertEqual(f.cache_get([2], {}), 2) class TestSingleDispatch(unittest.TestCase): def test_simple_overloads(self):