From 23ec435ac3ea3030569b2c0cb923e13680bdea5b Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Wed, 1 May 2019 12:23:11 -0700 Subject: [PATCH 1/5] Working version with docs --- Doc/library/functools.rst | 15 ++++++++++++++- Doc/whatsnew/3.8.rst | 14 ++++++++++++++ Lib/functools.py | 12 ++++++++---- Lib/test/test_functools.py | 19 ++++++++++++------- 4 files changed, 48 insertions(+), 12 deletions(-) diff --git a/Doc/library/functools.rst b/Doc/library/functools.rst index 16a779fa836858..01d4f82981168f 100644 --- a/Doc/library/functools.rst +++ b/Doc/library/functools.rst @@ -76,7 +76,8 @@ The :mod:`functools` module defines the following functions: .. versionadded:: 3.2 -.. decorator:: lru_cache(maxsize=128, typed=False) +.. decorator:: lru_cache(user_function) + lru_cache(maxsize=128, 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 @@ -90,6 +91,15 @@ The :mod:`functools` module defines the following functions: differ in their keyword argument order and may have two separate cache entries. + If *user_function* is specified, it must be a callable. This allows the + *lru_cache* decorator to be applied directly to a user function, leaving + the *maxsize* at its default value of 128:: + + @lru_cache + def summary_statistics(data): + data = list(data) + return len(data), min(data), mean(data), max(data), stdev(data) + If *maxsize* is set to ``None``, the LRU feature is disabled and the cache can grow without bound. The LRU feature performs best when *maxsize* is a power-of-two. @@ -165,6 +175,9 @@ The :mod:`functools` module defines the following functions: .. versionchanged:: 3.3 Added the *typed* option. + .. versionchanged:: 3.8 + Added the *user_function* option. + .. decorator:: total_ordering Given a class defining one or more rich comparison ordering methods, this diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index bbc55ddd63418d..01691cf782e805 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -244,6 +244,20 @@ where the DLL is stored (if a full or partial path is used to load the initial DLL) and paths added by :func:`~os.add_dll_directory`. +functools +--------- + +:func:`functools.lru_cache` can now be used as a straight decorator rather +as a function that returns a decorator. So both of these are now supported:: + + @lru_cache + def f(x): + ... + + @lru_cache(maxsize=256) + def f(x): + ... + datetime -------- diff --git a/Lib/functools.py b/Lib/functools.py index 1f1874db9b4cce..ef892348ed6272 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -517,14 +517,18 @@ def lru_cache(maxsize=128, typed=False): # The internals of the lru_cache are encapsulated for thread safety and # to allow the implementation to change (including a possible C version). - # Early detection of an erroneous call to @lru_cache without any arguments - # resulting in the inner function being passed to maxsize instead of an - # integer or None. Negative maxsize is treated as 0. if isinstance(maxsize, int): + # Negative maxsize is treated as 0 if maxsize < 0: maxsize = 0 + elif callable(maxsize) and isinstance(typed, bool): + # The user_function was passed in directly via the maxsize argument + user_function, maxsize = maxsize, 128 + wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo) + return update_wrapper(wrapper, user_function) elif maxsize is not None: - raise TypeError('Expected maxsize to be an integer or None') + raise TypeError( + 'Expected first argument to be an integer, a callable, or None') def decorating_function(user_function): wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo) diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 85c65d18326067..65c825ddf18ae3 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -1251,6 +1251,18 @@ def f(x): self.assertEqual(misses, 4) self.assertEqual(currsize, 2) + def test_lru_no_args(self): + @self.module.lru_cache + def square(x): + return x ** 2 + + self.assertEqual(list(map(square, [10, 20, 10])), + [100, 400, 100]) + self.assertEqual(square.cache_info().hits, 1) + self.assertEqual(square.cache_info().misses, 2) + self.assertEqual(square.cache_info().maxsize, 128) + self.assertEqual(square.cache_info().currsize, 2) + def test_lru_bug_35780(self): # C version of the lru_cache was not checking to see if # the user function call has already modified the cache @@ -1582,13 +1594,6 @@ def __eq__(self, other): self.assertEqual(test_func(DoubleEq(2)), # Trigger a re-entrant __eq__ call DoubleEq(2)) # Verify the correct return value - def test_early_detection_of_bad_call(self): - # Issue #22184 - with self.assertRaises(TypeError): - @functools.lru_cache - def f(): - pass - def test_lru_method(self): class X(int): f_cnt = 0 From da9fbf8c8ea2f484c7d57287027fff8c569dd398 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Wed, 1 May 2019 20:42:14 -0400 Subject: [PATCH 2/5] Add blurb --- Doc/whatsnew/3.8.rst | 3 +++ .../next/Library/2019-05-01-20-41-53.bpo-36772.fV2K0F.rst | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2019-05-01-20-41-53.bpo-36772.fV2K0F.rst diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index 01691cf782e805..6dc43ce2b2d810 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -258,6 +258,9 @@ as a function that returns a decorator. So both of these are now supported:: def f(x): ... +(Contributed by Raymond Hettinger in :issue:`36772`.) + + datetime -------- diff --git a/Misc/NEWS.d/next/Library/2019-05-01-20-41-53.bpo-36772.fV2K0F.rst b/Misc/NEWS.d/next/Library/2019-05-01-20-41-53.bpo-36772.fV2K0F.rst new file mode 100644 index 00000000000000..d78eec9ba7a5ae --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-05-01-20-41-53.bpo-36772.fV2K0F.rst @@ -0,0 +1,2 @@ +functools.lru_cache() can now be used as a straight decorator rather than as +a function that returns a decorator. From 7a41e5e67780cec34a8b067978ef8689925f4d93 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Wed, 1 May 2019 20:47:35 -0400 Subject: [PATCH 3/5] Minor wordsmithing --- Doc/whatsnew/3.8.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index 6dc43ce2b2d810..944386cb017382 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -248,7 +248,7 @@ functools --------- :func:`functools.lru_cache` can now be used as a straight decorator rather -as a function that returns a decorator. So both of these are now supported:: +than as a function returning a decorator. So both of these are now supported:: @lru_cache def f(x): From bb6be57cb58ebbada1d5f7d044697c5116edbd16 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Sun, 26 May 2019 09:46:05 -0700 Subject: [PATCH 4/5] Address review comment --- .../next/Library/2019-05-01-20-41-53.bpo-36772.fV2K0F.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2019-05-01-20-41-53.bpo-36772.fV2K0F.rst b/Misc/NEWS.d/next/Library/2019-05-01-20-41-53.bpo-36772.fV2K0F.rst index d78eec9ba7a5ae..00b8a684f9a7ac 100644 --- a/Misc/NEWS.d/next/Library/2019-05-01-20-41-53.bpo-36772.fV2K0F.rst +++ b/Misc/NEWS.d/next/Library/2019-05-01-20-41-53.bpo-36772.fV2K0F.rst @@ -1,2 +1,2 @@ -functools.lru_cache() can now be used as a straight decorator rather than as -a function that returns a decorator. +functools.lru_cache() can now be used as a straight decorator in +addition to its existing usage as a function that returns a decorator. From 750c8f8eac22c23e6a098b870a7e524c62ab46c9 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Sun, 26 May 2019 10:20:21 -0700 Subject: [PATCH 5/5] Switch to an example with a typically immutable argument --- Doc/library/functools.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/functools.rst b/Doc/library/functools.rst index 01d4f82981168f..8b8b1f80a622e7 100644 --- a/Doc/library/functools.rst +++ b/Doc/library/functools.rst @@ -96,9 +96,9 @@ The :mod:`functools` module defines the following functions: the *maxsize* at its default value of 128:: @lru_cache - def summary_statistics(data): - data = list(data) - return len(data), min(data), mean(data), max(data), stdev(data) + def count_vowels(sentence): + sentence = sentence.casefold() + return sum(sentence.count(vowel) for vowel in 'aeiou') If *maxsize* is set to ``None``, the LRU feature is disabled and the cache can grow without bound. The LRU feature performs best when *maxsize* is a