summaryrefslogtreecommitdiffstats
path: root/Lib/functools.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/functools.py')
-rw-r--r--Lib/functools.py179
1 files changed, 137 insertions, 42 deletions
diff --git a/Lib/functools.py b/Lib/functools.py
index 85ea257..226a46e 100644
--- a/Lib/functools.py
+++ b/Lib/functools.py
@@ -12,16 +12,22 @@ __all__ = ['update_wrapper', 'wraps', 'WRAPPER_ASSIGNMENTS', 'WRAPPER_UPDATES',
'total_ordering', 'cmp_to_key', 'lru_cache', 'reduce', 'partial']
from _functools import partial, reduce
-from collections import OrderedDict, namedtuple
+from collections import namedtuple
try:
from _thread import allocate_lock as Lock
except:
from _dummy_thread import allocate_lock as Lock
+
+################################################################################
+### update_wrapper() and wraps() decorator
+################################################################################
+
# update_wrapper() and wraps() are tools to help write
# wrapper functions that can handle naive introspection
-WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__', '__annotations__')
+WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
+ '__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
wrapped,
@@ -65,6 +71,11 @@ def wraps(wrapped,
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)
+
+################################################################################
+### total_ordering class decorator
+################################################################################
+
def total_ordering(cls):
"""Class decorator that fills in missing ordering methods"""
convert = {
@@ -93,6 +104,11 @@ def total_ordering(cls):
setattr(cls, opname, opfunc)
return cls
+
+################################################################################
+### cmp_to_key() function converter
+################################################################################
+
def cmp_to_key(mycmp):
"""Convert a cmp= function into a key= function"""
class K(object):
@@ -114,95 +130,174 @@ def cmp_to_key(mycmp):
__hash__ = None
return K
-_CacheInfo = namedtuple("CacheInfo", "hits misses maxsize currsize")
+try:
+ from _functools import cmp_to_key
+except ImportError:
+ pass
+
+
+################################################################################
+### LRU Cache function decorator
+################################################################################
+
+_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])
+
+class _HashedSeq(list):
+ __slots__ = 'hashvalue'
+
+ def __init__(self, tup, hash=hash):
+ self[:] = tup
+ self.hashvalue = hash(tup)
+
+ def __hash__(self):
+ return self.hashvalue
-def lru_cache(maxsize=100):
+def _make_key(args, kwds, typed,
+ kwd_mark = (object(),),
+ fasttypes = {int, str, frozenset, type(None)},
+ sorted=sorted, tuple=tuple, type=type, len=len):
+ 'Make a cache key from optionally typed positional and keyword arguments'
+ key = args
+ if kwds:
+ sorted_items = sorted(kwds.items())
+ key += kwd_mark
+ for item in sorted_items:
+ key += item
+ if typed:
+ key += tuple(type(v) for v in args)
+ if kwds:
+ key += tuple(type(v) for k, v in sorted_items)
+ elif len(key) == 1 and type(key[0]) in fasttypes:
+ return key[0]
+ return _HashedSeq(key)
+
+def lru_cache(maxsize=128, 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().
+ 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,
- tuple=tuple, sorted=sorted, len=len, KeyError=KeyError):
+ # Constants shared by all lru cache instances:
+ sentinel = object() # unique object used to signal cache misses
+ make_key = _make_key # build a key from the function arguments
+ PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields
+
+ def decorating_function(user_function):
- hits = misses = 0
- kwd_mark = (object(),) # separates positional and keyword args
- lock = Lock() # needed because OrderedDict isn't threadsafe
+ cache = {}
+ hits = misses = currsize = 0
+ full = False
+ cache_get = cache.get # bound method to lookup a key or return None
+ lock = Lock() # 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 is None:
- cache = dict() # simple cache without ordering or size limit
+ if maxsize == 0:
- @wraps(user_function)
def wrapper(*args, **kwds):
- nonlocal hits, misses
- key = args
- if kwds:
- key += kwd_mark + tuple(sorted(kwds.items()))
- try:
- result = cache[key]
+ # no caching, just a statistics update after a successful call
+ nonlocal misses
+ result = user_function(*args, **kwds)
+ misses += 1
+ return result
+
+ elif maxsize is None:
+
+ def wrapper(*args, **kwds):
+ # simple caching without ordering or size limit
+ nonlocal hits, misses, currsize
+ key = make_key(args, kwds, typed)
+ result = cache_get(key, sentinel)
+ if result is not sentinel:
hits += 1
return result
- except KeyError:
- pass
result = user_function(*args, **kwds)
cache[key] = result
misses += 1
+ currsize += 1
return result
+
else:
- cache = OrderedDict() # ordered least recent to most recent
- cache_popitem = cache.popitem
- cache_renew = cache.move_to_end
- @wraps(user_function)
def wrapper(*args, **kwds):
- nonlocal hits, misses
- key = args
- if kwds:
- key += kwd_mark + tuple(sorted(kwds.items()))
+ # size limited caching that tracks accesses by recency
+ nonlocal root, hits, misses, currsize, full
+ key = make_key(args, kwds, typed)
with lock:
- try:
- result = cache[key]
- cache_renew(key) # record recent use of this key
+ link = cache_get(key)
+ if link is not None:
+ # 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
- except KeyError:
- pass
result = user_function(*args, **kwds)
with lock:
- cache[key] = result # record recent use of this key
+ if key in cache:
+ # getting here means that this same key was added to the
+ # cache while the lock was released. since the link
+ # update is already done, we need only return the
+ # computed result and update the count of misses.
+ pass
+ elif full:
+ # 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 = root[NEXT]
+ del cache[root[KEY]]
+ root[KEY] = root[RESULT] = None
+ else:
+ # put result in a new link at the front of the queue
+ last = root[PREV]
+ link = [last, root, key, result]
+ cache[key] = last[NEXT] = root[PREV] = link
+ currsize += 1
+ full = (currsize == maxsize)
misses += 1
- if len(cache) > maxsize:
- cache_popitem(0) # purge least recently used cache entry
return result
def cache_info():
"""Report cache statistics"""
with lock:
- return _CacheInfo(hits, misses, maxsize, len(cache))
+ return _CacheInfo(hits, misses, maxsize, currsize)
def cache_clear():
"""Clear the cache and cache statistics"""
- nonlocal hits, misses
+ nonlocal hits, misses, currsize, full
with lock:
cache.clear()
- hits = misses = 0
+ root[:] = [root, root, None, None]
+ hits = misses = currsize = 0
+ full = False
wrapper.cache_info = cache_info
wrapper.cache_clear = cache_clear
- return wrapper
+ return update_wrapper(wrapper, user_function)
return decorating_function