diff --git a/Doc/library/functools.rst b/Doc/library/functools.rst index 2316e804899..4eaf54e97e4 100644 --- a/Doc/library/functools.rst +++ b/Doc/library/functools.rst @@ -40,7 +40,7 @@ The :mod:`functools` module defines the following functions: .. versionadded:: 3.2 -.. decorator:: lru_cache(maxsize=100) +.. decorator:: lru_cache(maxsize=100, typed=False) Decorator to wrap a function with a memoizing callable that saves up to the *maxsize* most recent calls. It can save time when an expensive or I/O bound @@ -52,6 +52,10 @@ The :mod:`functools` module defines the following functions: If *maxsize* is set to None, the LRU feature is disabled and the cache can grow without bound. + If *typed* is set to True, function arguments of different types will be + cached separately. For example, ``f(3)`` and ``f(3.0)`` will be treated + as distinct calls with distinct results. + To help measure the effectiveness of the cache and tune the *maxsize* parameter, the wrapped function is instrumented with a :func:`cache_info` function that returns a :term:`named tuple` showing *hits*, *misses*, @@ -67,8 +71,8 @@ The :mod:`functools` module defines the following functions: An `LRU (least recently used) cache `_ works - best when more recent calls are the best predictors of upcoming calls (for - example, the most popular articles on a news server tend to change daily). + best when the most recent calls are the best predictors of upcoming calls (for + example, the most popular articles on a news server tend to change each day). The cache's size limit assures that the cache does not grow without bound on long-running processes such as web servers. @@ -111,6 +115,9 @@ The :mod:`functools` module defines the following functions: .. versionadded:: 3.2 + .. versionchanged:: 3.3 + Added the *typed* option. + .. decorator:: total_ordering Given a class defining one or more rich comparison ordering methods, this diff --git a/Lib/functools.py b/Lib/functools.py index 038f284f068..1abb37a8c86 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -121,12 +121,16 @@ except ImportError: _CacheInfo = namedtuple("CacheInfo", "hits misses maxsize currsize") -def lru_cache(maxsize=100): +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 @@ -142,7 +146,7 @@ def lru_cache(maxsize=100): # to allow the implementation to change (including a possible C version). def decorating_function(user_function, - tuple=tuple, sorted=sorted, len=len, KeyError=KeyError): + *, tuple=tuple, sorted=sorted, map=map, len=len, type=type, KeyError=KeyError): hits = misses = 0 kwd_mark = (object(),) # separates positional and keyword args @@ -156,7 +160,12 @@ def lru_cache(maxsize=100): nonlocal hits, misses key = args if kwds: - key += kwd_mark + tuple(sorted(kwds.items())) + sorted_items = tuple(sorted(kwds.items())) + key += kwd_mark + sorted_items + if typed: + key += tuple(map(type, args)) + if kwds: + key += tuple(type(v) for k, v in sorted_items) try: result = cache[key] hits += 1 @@ -177,7 +186,12 @@ def lru_cache(maxsize=100): nonlocal hits, misses key = args if kwds: - key += kwd_mark + tuple(sorted(kwds.items())) + sorted_items = tuple(sorted(kwds.items())) + key += kwd_mark + sorted_items + if typed: + key += tuple(map(type, args)) + if kwds: + key += tuple(type(v) for k, v in sorted_items) with lock: try: result = cache[key] diff --git a/Lib/re.py b/Lib/re.py index cdf597627f0..4b90b3f3288 100644 --- a/Lib/re.py +++ b/Lib/re.py @@ -207,7 +207,7 @@ def compile(pattern, flags=0): def purge(): "Clear the regular expression caches" - _compile_typed.cache_clear() + _compile.cache_clear() _compile_repl.cache_clear() def template(pattern, flags=0): @@ -253,11 +253,8 @@ def escape(pattern): _pattern_type = type(sre_compile.compile("", 0)) +@functools.lru_cache(maxsize=500, typed=True) def _compile(pattern, flags): - return _compile_typed(type(pattern), pattern, flags) - -@functools.lru_cache(maxsize=500) -def _compile_typed(text_bytes_type, pattern, flags): # internal: compile pattern if isinstance(pattern, _pattern_type): if flags: diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index a31c92ee790..c4d9fe6ac27 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -734,6 +734,22 @@ class TestLRU(unittest.TestCase): with self.assertRaises(IndexError): func(15) + def test_lru_with_types(self): + for maxsize in (None, 100): + @functools.lru_cache(maxsize=maxsize, typed=True) + def square(x): + return x * x + self.assertEqual(square(3), 9) + self.assertEqual(type(square(3)), type(9)) + self.assertEqual(square(3.0), 9.0) + self.assertEqual(type(square(3.0)), type(9.0)) + self.assertEqual(square(x=3), 9) + self.assertEqual(type(square(x=3)), type(9)) + self.assertEqual(square(x=3.0), 9.0) + self.assertEqual(type(square(x=3.0)), type(9.0)) + self.assertEqual(square.cache_info().hits, 4) + self.assertEqual(square.cache_info().misses, 4) + def test_main(verbose=None): test_classes = ( TestPartial, diff --git a/Misc/NEWS b/Misc/NEWS index cd6747e1539..3fe419b2bde 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -319,6 +319,9 @@ Core and Builtins Library ------- +- Issue #13227: functools.lru_cache() now has a option to distinguish + calls with different argument types. + - Issue #6090: zipfile raises a ValueError when a document with a timestamp earlier than 1980 is provided. Patch contributed by Petri Lehtinen.