functools_lru_cache.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. from __future__ import absolute_import
  2. import functools
  3. from collections import namedtuple
  4. from threading import RLock
  5. _CacheInfo = namedtuple("_CacheInfo", ["hits", "misses", "maxsize", "currsize"])
  6. @functools.wraps(functools.update_wrapper)
  7. def update_wrapper(
  8. wrapper,
  9. wrapped,
  10. assigned=functools.WRAPPER_ASSIGNMENTS,
  11. updated=functools.WRAPPER_UPDATES,
  12. ):
  13. """
  14. Patch two bugs in functools.update_wrapper.
  15. """
  16. # workaround for http://bugs.python.org/issue3445
  17. assigned = tuple(attr for attr in assigned if hasattr(wrapped, attr))
  18. wrapper = functools.update_wrapper(wrapper, wrapped, assigned, updated)
  19. # workaround for https://bugs.python.org/issue17482
  20. wrapper.__wrapped__ = wrapped
  21. return wrapper
  22. class _HashedSeq(list):
  23. __slots__ = 'hashvalue'
  24. def __init__(self, tup, hash=hash):
  25. self[:] = tup
  26. self.hashvalue = hash(tup)
  27. def __hash__(self):
  28. return self.hashvalue
  29. def _make_key(
  30. args,
  31. kwds,
  32. typed,
  33. kwd_mark=(object(),),
  34. fasttypes=set([int, str, frozenset, type(None)]),
  35. sorted=sorted,
  36. tuple=tuple,
  37. type=type,
  38. len=len,
  39. ):
  40. 'Make a cache key from optionally typed positional and keyword arguments'
  41. key = args
  42. if kwds:
  43. sorted_items = sorted(kwds.items())
  44. key += kwd_mark
  45. for item in sorted_items:
  46. key += item
  47. if typed:
  48. key += tuple(type(v) for v in args)
  49. if kwds:
  50. key += tuple(type(v) for k, v in sorted_items)
  51. elif len(key) == 1 and type(key[0]) in fasttypes:
  52. return key[0]
  53. return _HashedSeq(key)
  54. def lru_cache(maxsize=100, typed=False): # noqa: C901
  55. """Least-recently-used cache decorator.
  56. If *maxsize* is set to None, the LRU features are disabled and the cache
  57. can grow without bound.
  58. If *typed* is True, arguments of different types will be cached separately.
  59. For example, f(3.0) and f(3) will be treated as distinct calls with
  60. distinct results.
  61. Arguments to the cached function must be hashable.
  62. View the cache statistics named tuple (hits, misses, maxsize, currsize) with
  63. f.cache_info(). Clear the cache and statistics with f.cache_clear().
  64. Access the underlying function with f.__wrapped__.
  65. See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used
  66. """
  67. # Users should only access the lru_cache through its public API:
  68. # cache_info, cache_clear, and f.__wrapped__
  69. # The internals of the lru_cache are encapsulated for thread safety and
  70. # to allow the implementation to change (including a possible C version).
  71. def decorating_function(user_function):
  72. cache = dict()
  73. stats = [0, 0] # make statistics updateable non-locally
  74. HITS, MISSES = 0, 1 # names for the stats fields
  75. make_key = _make_key
  76. cache_get = cache.get # bound method to lookup key or return None
  77. _len = len # localize the global len() function
  78. lock = RLock() # because linkedlist updates aren't threadsafe
  79. root = [] # root of the circular doubly linked list
  80. root[:] = [root, root, None, None] # initialize by pointing to self
  81. nonlocal_root = [root] # make updateable non-locally
  82. PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields
  83. if maxsize == 0:
  84. def wrapper(*args, **kwds):
  85. # no caching, just do a statistics update after a successful call
  86. result = user_function(*args, **kwds)
  87. stats[MISSES] += 1
  88. return result
  89. elif maxsize is None:
  90. def wrapper(*args, **kwds):
  91. # simple caching without ordering or size limit
  92. key = make_key(args, kwds, typed)
  93. result = cache_get(
  94. key, root
  95. ) # root used here as a unique not-found sentinel
  96. if result is not root:
  97. stats[HITS] += 1
  98. return result
  99. result = user_function(*args, **kwds)
  100. cache[key] = result
  101. stats[MISSES] += 1
  102. return result
  103. else:
  104. def wrapper(*args, **kwds):
  105. # size limited caching that tracks accesses by recency
  106. key = make_key(args, kwds, typed) if kwds or typed else args
  107. with lock:
  108. link = cache_get(key)
  109. if link is not None:
  110. # record recent use of the key by moving it
  111. # to the front of the list
  112. (root,) = nonlocal_root
  113. link_prev, link_next, key, result = link
  114. link_prev[NEXT] = link_next
  115. link_next[PREV] = link_prev
  116. last = root[PREV]
  117. last[NEXT] = root[PREV] = link
  118. link[PREV] = last
  119. link[NEXT] = root
  120. stats[HITS] += 1
  121. return result
  122. result = user_function(*args, **kwds)
  123. with lock:
  124. (root,) = nonlocal_root
  125. if key in cache:
  126. # getting here means that this same key was added to the
  127. # cache while the lock was released. since the link
  128. # update is already done, we need only return the
  129. # computed result and update the count of misses.
  130. pass
  131. elif _len(cache) >= maxsize:
  132. # use the old root to store the new key and result
  133. oldroot = root
  134. oldroot[KEY] = key
  135. oldroot[RESULT] = result
  136. # empty the oldest link and make it the new root
  137. root = nonlocal_root[0] = oldroot[NEXT]
  138. oldkey = root[KEY]
  139. root[KEY] = root[RESULT] = None
  140. # now update the cache dictionary for the new links
  141. del cache[oldkey]
  142. cache[key] = oldroot
  143. else:
  144. # put result in a new link at the front of the list
  145. last = root[PREV]
  146. link = [last, root, key, result]
  147. last[NEXT] = root[PREV] = cache[key] = link
  148. stats[MISSES] += 1
  149. return result
  150. def cache_info():
  151. """Report cache statistics"""
  152. with lock:
  153. return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache))
  154. def cache_clear():
  155. """Clear the cache and cache statistics"""
  156. with lock:
  157. cache.clear()
  158. root = nonlocal_root[0]
  159. root[:] = [root, root, None, None]
  160. stats[:] = [0, 0]
  161. wrapper.__wrapped__ = user_function
  162. wrapper.cache_info = cache_info
  163. wrapper.cache_clear = cache_clear
  164. return update_wrapper(wrapper, user_function)
  165. return decorating_function