summaryrefslogtreecommitdiff
path: root/Lib/functools.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/functools.py')
-rw-r--r--Lib/functools.py111
1 files changed, 81 insertions, 30 deletions
diff --git a/Lib/functools.py b/Lib/functools.py
index 85ea257abd..f4c8271bc9 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,14 +130,28 @@ 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
+
-def lru_cache(maxsize=100):
+################################################################################
+### LRU Cache function decorator
+################################################################################
+
+_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
@@ -136,57 +166,77 @@ def lru_cache(maxsize=100):
# 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):
+ def decorating_function(user_function):
+ cache = dict()
hits = misses = 0
- kwd_mark = (object(),) # separates positional and keyword args
- lock = Lock() # needed because OrderedDict isn't threadsafe
+ cache_get = cache.get # bound method to lookup key or return None
+ _len = len # localize the global len() function
+ kwd_mark = (object(),) # separate positional and keyword args
+ 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
+ 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):
+ 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 is None:
- cache = dict() # simple cache without ordering or size limit
@wraps(user_function)
def wrapper(*args, **kwds):
+ # simple caching without ordering or size limit
nonlocal hits, misses
- key = args
- if kwds:
- key += kwd_mark + tuple(sorted(kwds.items()))
- try:
- result = cache[key]
+ 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:
hits += 1
return result
- except KeyError:
- pass
result = user_function(*args, **kwds)
cache[key] = result
misses += 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):
+ # size limited caching that tracks accesses by recency
nonlocal hits, misses
- key = args
- if kwds:
- key += kwd_mark + tuple(sorted(kwds.items()))
+ key = make_key(args, kwds, typed) if kwds or typed else args
with lock:
- try:
- result = cache[key]
- cache_renew(key) # record recent use of this key
+ link = cache_get(key)
+ if link is not None:
+ # record recent use of the key by moving it to the front of the list
+ 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
+ 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]
misses += 1
- if len(cache) > maxsize:
- cache_popitem(0) # purge least recently used cache entry
return result
def cache_info():
@@ -199,6 +249,7 @@ def lru_cache(maxsize=100):
nonlocal hits, misses
with lock:
cache.clear()
+ root[:] = [root, root, None, None]
hits = misses = 0
wrapper.cache_info = cache_info