From ffc6e17473a5395455cccd9bd834938b2630b0a4 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Thu, 15 Apr 2021 17:32:11 -0500 Subject: [PATCH 1/6] Steps towards more symbolic dict keys in memoize_on_first_arg, memoize_method --- pytools/__init__.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pytools/__init__.py b/pytools/__init__.py index 688c9cd9..617966bc 100644 --- a/pytools/__init__.py +++ b/pytools/__init__.py @@ -666,10 +666,6 @@ def wrapper(*args): FunctionValueCache = memoize -class _HasKwargs: - pass - - def memoize_on_first_arg(function, cache_dict_name=None): """Like :func:`memoize_method`, but for functions that take the object in which do memoization information is stored as first argument. @@ -678,18 +674,19 @@ def memoize_on_first_arg(function, cache_dict_name=None): """ if cache_dict_name is None: - cache_dict_name = intern( - f"_memoize_dic_{function.__module__}{function.__name__}" - ) + cache_dict_name = (memoize_on_first_arg, function) + else: + if not isinstance(cache_dict_name, tuple): + cache_dict_name = (cache_dict_name,) def wrapper(obj, *args, **kwargs): if kwargs: - key = (_HasKwargs, frozenset(kwargs.items())) + args + key = cache_dict_name + args + (frozenset(kwargs.items()),) else: - key = args + key = (cache_dict_name + args) try: - return getattr(obj, cache_dict_name)[key] + return obj._memoize_on_first_arg_dict[key] except AttributeError: attribute_error = True except KeyError: @@ -697,14 +694,14 @@ def wrapper(obj, *args, **kwargs): result = function(obj, *args, **kwargs) if attribute_error: - object.__setattr__(obj, cache_dict_name, {key: result}) + object.__setattr__(obj, "_memoize_on_first_arg_dict", {key: result}) return result else: - getattr(obj, cache_dict_name)[key] = result + obj._memoize_on_first_arg_dict[key] = result return result def clear_cache(obj): - object.__delattr__(obj, cache_dict_name) + del obj._memoize_on_first_arg_dict from functools import update_wrapper new_wrapper = update_wrapper(wrapper, function) @@ -722,8 +719,7 @@ def memoize_method(method: F) -> F: (e.g. by overwritting ``__setattr__``), e.g. frozen :mod:`dataclasses`. """ - return memoize_on_first_arg(method, - intern(f"_memoize_dic_{method.__name__}")) + return memoize_on_first_arg(method, (memoize_method, method)) class keyed_memoize_on_first_arg: # noqa: N801 @@ -798,6 +794,10 @@ def _default_cache_dict_name(self, function): return intern(f"_memoize_dic_{function.__name__}") +class _HasKwargs: + pass + + def memoize_method_with_uncached(uncached_args=None, uncached_kwargs=None): """Supports cache deletion via ``method_name.clear_cache(self)``. From 9710eeff52ccfcd552030cd8754b6759ff639e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kl=C3=B6ckner?= Date: Fri, 16 Apr 2021 16:58:29 -0500 Subject: [PATCH 2/6] Use object.__delattr__ in memoize_first_arg.clear_cache --- pytools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytools/__init__.py b/pytools/__init__.py index 617966bc..056d888c 100644 --- a/pytools/__init__.py +++ b/pytools/__init__.py @@ -701,7 +701,7 @@ def wrapper(obj, *args, **kwargs): return result def clear_cache(obj): - del obj._memoize_on_first_arg_dict + object.__delattr__("_memoize_on_first_arg_dict") from functools import update_wrapper new_wrapper = update_wrapper(wrapper, function) From 6d440d1efa78e85a69e8c4b94add0f8bf0708d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kl=C3=B6ckner?= Date: Fri, 16 Apr 2021 17:03:33 -0500 Subject: [PATCH 3/6] Add missing self arg to __delattr__ --- pytools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytools/__init__.py b/pytools/__init__.py index 056d888c..b78ea772 100644 --- a/pytools/__init__.py +++ b/pytools/__init__.py @@ -701,7 +701,7 @@ def wrapper(obj, *args, **kwargs): return result def clear_cache(obj): - object.__delattr__("_memoize_on_first_arg_dict") + object.__delattr__(self, "_memoize_on_first_arg_dict") from functools import update_wrapper new_wrapper = update_wrapper(wrapper, function) From 8f793ea188c7a4ded3f0c331e0fa791f2857a1b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kl=C3=B6ckner?= Date: Fri, 16 Apr 2021 17:32:55 -0500 Subject: [PATCH 4/6] Fix __delattr__ invocation, yet again --- pytools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytools/__init__.py b/pytools/__init__.py index b78ea772..ec00862e 100644 --- a/pytools/__init__.py +++ b/pytools/__init__.py @@ -701,7 +701,7 @@ def wrapper(obj, *args, **kwargs): return result def clear_cache(obj): - object.__delattr__(self, "_memoize_on_first_arg_dict") + object.__delattr__(obj, "_memoize_on_first_arg_dict") from functools import update_wrapper new_wrapper = update_wrapper(wrapper, function) From 219e1110d57ba0ec00c2ac56f73214b5d8a46c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kl=C3=B6ckner?= Date: Sat, 17 Apr 2021 15:48:53 -0500 Subject: [PATCH 5/6] Clarify behavior in nested functions --- pytools/__init__.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/pytools/__init__.py b/pytools/__init__.py index ec00862e..e84aeebc 100644 --- a/pytools/__init__.py +++ b/pytools/__init__.py @@ -671,6 +671,13 @@ def memoize_on_first_arg(function, cache_dict_name=None): in which do memoization information is stored as first argument. Supports cache deletion via ``function_name.clear_cache(self)``. + + .. note:: + + If *function* is nested in another function, a separate cache + is created for each invocation of the surrounding function, + and use of the function in the cache key may keep references + to the nested function alive longer than desired. """ if cache_dict_name is None: @@ -723,9 +730,9 @@ def memoize_method(method: F) -> F: class keyed_memoize_on_first_arg: # noqa: N801 - """Like :func:`memoize_method`, but for functions that take the object - in which memoization information is stored as first argument. - + """Like :func:`memoize_on_first_arg`, but uses a user-supplied function + for cache key computation. + Supports cache deletion via ``function_name.clear_cache(self)``. :arg key: A function receiving the same arguments as the decorated function @@ -734,6 +741,13 @@ class keyed_memoize_on_first_arg: # noqa: N801 used to hold the cache. .. versionadded :: 2020.3 + + .. note:: + + If *function* is nested in another function, a separate cache + is created for each invocation of the surrounding function, + and use of the function in the cache key may keep references + to the nested function alive longer than desired. """ def __init__(self, key, cache_dict_name=None): From ebea5a4f77666f01b44dc6516518298fc817ae93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kl=C3=B6ckner?= Date: Sat, 17 Apr 2021 15:59:33 -0500 Subject: [PATCH 6/6] Delete whitespace from line (appease flake8) --- pytools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytools/__init__.py b/pytools/__init__.py index e84aeebc..b68002f9 100644 --- a/pytools/__init__.py +++ b/pytools/__init__.py @@ -732,7 +732,7 @@ def memoize_method(method: F) -> F: class keyed_memoize_on_first_arg: # noqa: N801 """Like :func:`memoize_on_first_arg`, but uses a user-supplied function for cache key computation. - + Supports cache deletion via ``function_name.clear_cache(self)``. :arg key: A function receiving the same arguments as the decorated function