-
-
Notifications
You must be signed in to change notification settings - Fork 35k
gh-102618: Introduce a functools.cached_method decorator
#150002
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a5b4b0d
1425819
8ff6b31
30f839b
9d109fa
d26358a
0686932
262a53e
fffc5d3
57cc653
06c2b18
365c484
981013b
8086aa5
a7f206c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,15 +12,15 @@ | |||||||||||||||||||||||||||||||
| __all__ = ['update_wrapper', 'wraps', 'WRAPPER_ASSIGNMENTS', 'WRAPPER_UPDATES', | ||||||||||||||||||||||||||||||||
| 'total_ordering', 'cache', 'cmp_to_key', 'lru_cache', 'reduce', | ||||||||||||||||||||||||||||||||
| 'partial', 'partialmethod', 'singledispatch', 'singledispatchmethod', | ||||||||||||||||||||||||||||||||
| 'cached_property', 'Placeholder'] | ||||||||||||||||||||||||||||||||
| 'cached_method', 'cached_property', 'Placeholder'] | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| from abc import get_cache_token | ||||||||||||||||||||||||||||||||
| from collections import namedtuple | ||||||||||||||||||||||||||||||||
| # import weakref # Deferred to single_dispatch() | ||||||||||||||||||||||||||||||||
| from operator import itemgetter | ||||||||||||||||||||||||||||||||
| from reprlib import recursive_repr | ||||||||||||||||||||||||||||||||
| from types import FunctionType, GenericAlias, MethodType, MappingProxyType, UnionType | ||||||||||||||||||||||||||||||||
| from _thread import RLock | ||||||||||||||||||||||||||||||||
| lazy import weakref | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| ################################################################################ | ||||||||||||||||||||||||||||||||
| ### update_wrapper() and wraps() decorator | ||||||||||||||||||||||||||||||||
|
|
@@ -907,11 +907,6 @@ def singledispatch(func): | |||||||||||||||||||||||||||||||
| implementations can be registered using the register() attribute of the | ||||||||||||||||||||||||||||||||
| generic function. | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
| # There are many programs that use functools without singledispatch, so we | ||||||||||||||||||||||||||||||||
| # trade-off making singledispatch marginally slower for the benefit of | ||||||||||||||||||||||||||||||||
| # making start-up of such applications slightly faster. | ||||||||||||||||||||||||||||||||
| import weakref | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| registry = {} | ||||||||||||||||||||||||||||||||
| dispatch_cache = weakref.WeakKeyDictionary() | ||||||||||||||||||||||||||||||||
| cache_token = None | ||||||||||||||||||||||||||||||||
|
|
@@ -1132,6 +1127,86 @@ def __wrapped__(self): | |||||||||||||||||||||||||||||||
| def register(self): | ||||||||||||||||||||||||||||||||
| return self._unbound.register | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| ################################################################################ | ||||||||||||||||||||||||||||||||
| ### cached_method -- a version of lru_cache() which uses id(self) | ||||||||||||||||||||||||||||||||
| ################################################################################ | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def _cached_method_weakref_callback(cache_dict, id_key): | ||||||||||||||||||||||||||||||||
| def callback(ref): | ||||||||||||||||||||||||||||||||
| cache_dict.pop(id_key, None) | ||||||||||||||||||||||||||||||||
| return callback | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def _wrap_unbound_cached_method(ref, unbound_method, maxsize, typed): | ||||||||||||||||||||||||||||||||
| @lru_cache(maxsize, typed) | ||||||||||||||||||||||||||||||||
| def wrapped(*args, **kwargs): | ||||||||||||||||||||||||||||||||
| return unbound_method(ref(), *args, **kwargs) | ||||||||||||||||||||||||||||||||
| return wrapped | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| class _cached_method: | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
| A caching decorator for use on instance methods. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Using cache or lru_cache on methods is problematic because the instance is put into | ||||||||||||||||||||||||||||||||
| the cache and cannot be garbage collected until the cache is cleared. This decorator | ||||||||||||||||||||||||||||||||
| uses a cache based on `id(self)` and a weakref to clear cache entries. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| The instance must be weak-referencable. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| By default, this provides an infinite sized cache similar to functools.cache. Use | ||||||||||||||||||||||||||||||||
| *maxsize* and *typed* to set these attributes of the underlying LRU cache. | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
| def __init__(self, func, /, maxsize=None, typed=False): | ||||||||||||||||||||||||||||||||
| self._function_table = {} | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| self._maxsize = maxsize | ||||||||||||||||||||||||||||||||
| self._typed = typed | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| self.func = func | ||||||||||||||||||||||||||||||||
| update_wrapper(self, func) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def __call__(self, instance, *args, **kwargs): | ||||||||||||||||||||||||||||||||
| cached_func = self._get_or_create_cached_func(instance) | ||||||||||||||||||||||||||||||||
| return cached_func(*args, **kwargs) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def __get__(self, instance, owner=None): | ||||||||||||||||||||||||||||||||
| if instance is None: | ||||||||||||||||||||||||||||||||
| return self | ||||||||||||||||||||||||||||||||
| return self._get_or_create_cached_func(instance) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def _get_or_create_cached_func(self, instance): | ||||||||||||||||||||||||||||||||
| instance_id = id(instance) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||
| ref, cached_func = self._function_table[instance_id] | ||||||||||||||||||||||||||||||||
| except KeyError: | ||||||||||||||||||||||||||||||||
| ref = weakref.ref( | ||||||||||||||||||||||||||||||||
| instance, | ||||||||||||||||||||||||||||||||
| _cached_method_weakref_callback( | ||||||||||||||||||||||||||||||||
| self._function_table, instance_id | ||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| cached_func = _wrap_unbound_cached_method( | ||||||||||||||||||||||||||||||||
| ref, self.func, self._maxsize, self._typed | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| self._function_table[instance_id] = ref, cached_func | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return cached_func | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def cached_method(func=None, /, maxsize=None, typed=False): | ||||||||||||||||||||||||||||||||
|
sirosen marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||
| if maxsize is not None and not isinstance(maxsize, int) and not callable(maxsize): | ||||||||||||||||||||||||||||||||
| raise TypeError( | ||||||||||||||||||||||||||||||||
| 'Expected maxsize to be an integer, a callable, or None') | ||||||||||||||||||||||||||||||||
| if func is None: | ||||||||||||||||||||||||||||||||
| def decorator(func): | ||||||||||||||||||||||||||||||||
| return _cached_method(func, maxsize=maxsize, typed=typed) | ||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Lines 586 to 600 in 1604043
I think we should do it here too to avoid confusing errors.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It came out slightly differently in 8086aa5 because the signatures are a bit different, but the idea is the same.
|
||||||||||||||||||||||||||||||||
| return decorator | ||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||
| return _cached_method(func, maxsize=maxsize, typed=typed) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| ################################################################################ | ||||||||||||||||||||||||||||||||
| ### cached_property() - property result cached as instance attribute | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| Added a new ``functools.cached_method`` decorator, which uses weakrefs and | ||
| avoids putting ``self`` into the cache. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Issue is preferred here IIRC, right Hugo?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that right? That seems odd. "Contributed by X in Y" suggests to me that "Y" is the implementation (PR), not specification (issue).