from collections import namedtuple
from functools import update_wrapper
from threading import Lock
_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])
def lru_cache(maxsize=100, typed=False):
"""Least-recently-used cache decorator.
If *maxsize* is set to None, the LRU features are disabled and the cache
can grow without bound.
If *typed* is True, arguments of different types will be cached separately.
For example, f(3.0) and f(3) will be treated as distinct calls with
distinct results.
Arguments to the cached function must be hashable.
View the cache statistics named tuple (hits, misses, maxsize, currsize) with
f.cache_info(). Clear the cache and statistics with f.cache_clear().
Access the underlying function with f.__wrapped__.
See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used
"""
# Users should only access the lru_cache through its public API:
# cache_info, cache_clear, and f.__wrapped__
# The internals of the lru_cache are encapsulated for thread safety and
# to allow the implementation to change (including a possible C version).
def decorating_function(user_function):
cache = dict()
stats = [0, 0] # make statistics updateable non-locally
HITS, MISSES = 0, 1 # names for the stats fields
kwd_mark = (object(),) # separate positional and keyword args
cache_get = cache.get # bound method to lookup key or return None
_len = len # localize the global len() function
lock = Lock() # because linkedlist updates aren't threadsafe
root = [] # root of the circular doubly linked list
nonlocal_root = [root] # make updateable non-locally
root[:] = [root, root, None, None] # initialize by pointing to self
PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields
def make_key(args, kwds, typed, tuple=tuple, sorted=sorted, type=type):
# helper function to build a cache key from positional and keyword args
key = args
if kwds:
sorted_items = tuple(sorted(kwds.items()))
key += kwd_mark + sorted_items
if typed:
key += tuple(type(v) for v in args)
if kwds:
key += tuple(type(v) for k, v in sorted_items)
return key
if maxsize == 0:
def wrapper(*args, **kwds):
# no caching, just do a statistics update after a successful call
result = user_function(*args, **kwds)
stats[MISSES] += 1
return result
elif maxsize is None:
def wrapper(*args, **kwds):
# simple caching without ordering or size limit
key = make_key(args, kwds, typed) if kwds or typed else args
result = cache_get(key, root) # root used here as a unique not-found sentinel
if result is not root:
stats[HITS] += 1
return result
result = user_function(*args, **kwds)
cache[key] = result
stats[MISSES] += 1
return result
else:
def wrapper(*args, **kwds):
# size limited caching that tracks accesses by recency
key = make_key(args, kwds, typed) if kwds or typed else args
with lock:
link = cache_get(key)
if link is not None:
# record recent use of the key by moving it to the front of the list
root, = nonlocal_root
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
stats[HITS] += 1
return result
result = user_function(*args, **kwds)
with lock:
root = nonlocal_root[0]
if _len(cache) < maxsize:
# put result in a new link at the front of the list
last = root[PREV]
link = [last, root, key, result]
cache[key] = last[NEXT] = root[PREV] = link
else:
# use root to store the new key and result
root[KEY] = key
root[RESULT] = result
cache[key] = root
# empty the oldest link and make it the new root
root = nonlocal_root[0] = root[NEXT]
del cache[root[KEY]]
root[KEY] = None
root[RESULT] = None
stats[MISSES] += 1
return result
def cache_info():
"""Report cache statistics"""
with lock:
return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache))
def cache_clear():
"""Clear the cache and cache statistics"""
with lock:
cache.clear()
root = nonlocal_root[0]
root[:] = [root, root, None, None]
stats[:] = [0, 0]
wrapper.__wrapped__ = user_function
wrapper.cache_info = cache_info
wrapper.cache_clear = cache_clear
return update_wrapper(wrapper, user_function)
return decorating_function
Diff to Previous Revision
--- revision 4 2012-03-17 23:42:52
+++ revision 5 2012-03-31 03:12:26
@@ -1,5 +1,5 @@
from collections import namedtuple
-from functools import wraps
+from functools import update_wrapper
from threading import Lock
_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])
@@ -23,6 +23,7 @@
See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used
"""
+
# Users should only access the lru_cache through its public API:
# cache_info, cache_clear, and f.__wrapped__
# The internals of the lru_cache are encapsulated for thread safety and
@@ -31,13 +32,14 @@
def decorating_function(user_function):
cache = dict()
- stats = [0, 0]
+ stats = [0, 0] # make statistics updateable non-locally
HITS, MISSES = 0, 1 # names for the stats fields
kwd_mark = (object(),) # separate positional and keyword args
cache_get = cache.get # bound method to lookup key or return None
_len = len # localize the global len() function
lock = Lock() # because linkedlist updates aren't threadsafe
root = [] # root of the circular doubly linked list
+ nonlocal_root = [root] # make updateable non-locally
root[:] = [root, root, None, None] # initialize by pointing to self
PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields
@@ -55,7 +57,6 @@
if maxsize == 0:
- @wraps(user_function)
def wrapper(*args, **kwds):
# no caching, just do a statistics update after a successful call
result = user_function(*args, **kwds)
@@ -64,7 +65,6 @@
elif maxsize is None:
- @wraps(user_function)
def wrapper(*args, **kwds):
# simple caching without ordering or size limit
key = make_key(args, kwds, typed) if kwds or typed else args
@@ -79,7 +79,6 @@
else:
- @wraps(user_function)
def wrapper(*args, **kwds):
# size limited caching that tracks accesses by recency
key = make_key(args, kwds, typed) if kwds or typed else args
@@ -87,6 +86,7 @@
link = cache_get(key)
if link is not None:
# record recent use of the key by moving it to the front of the list
+ root, = nonlocal_root
link_prev, link_next, key, result = link
link_prev[NEXT] = link_next
link_next[PREV] = link_prev
@@ -98,15 +98,22 @@
return result
result = user_function(*args, **kwds)
with lock:
- last = root[PREV]
- link = [last, root, key, result]
- cache[key] = last[NEXT] = root[PREV] = link
- if _len(cache) > maxsize:
- # purge least recently used cache entry
- old_prev, old_next, old_key, old_result = root[NEXT]
- root[NEXT] = old_next
- old_next[PREV] = root
- del cache[old_key]
+ root = nonlocal_root[0]
+ if _len(cache) < maxsize:
+ # put result in a new link at the front of the list
+ last = root[PREV]
+ link = [last, root, key, result]
+ cache[key] = last[NEXT] = root[PREV] = link
+ else:
+ # use root to store the new key and result
+ root[KEY] = key
+ root[RESULT] = result
+ cache[key] = root
+ # empty the oldest link and make it the new root
+ root = nonlocal_root[0] = root[NEXT]
+ del cache[root[KEY]]
+ root[KEY] = None
+ root[RESULT] = None
stats[MISSES] += 1
return result
@@ -119,12 +126,13 @@
"""Clear the cache and cache statistics"""
with lock:
cache.clear()
+ root = nonlocal_root[0]
root[:] = [root, root, None, None]
stats[:] = [0, 0]
wrapper.__wrapped__ = user_function
wrapper.cache_info = cache_info
wrapper.cache_clear = cache_clear
- return wrapper
+ return update_wrapper(wrapper, user_function)
return decorating_function