Repository navigation
bpo-21145: Add cached_property decorator in functools #6982
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
Changes from 1 commit
1cda951
72259cd
26f3f43
567e819
f7876d0
354a975
f006f68
d6bed4b
b41380d
b504a20
977ece0
5bdeaab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,34 @@ function for the purposes of this module. | |
|
|
||
| The :mod:`functools` module defines the following functions: | ||
|
|
||
| .. decorator:: cached_property(func) | ||
|
|
||
| Transform a method of a class into a property whose value is computed once | ||
| and then cached as a normal attribute for the life of the instance. Similar | ||
| to :func:`property`, with the addition of caching. Useful for expensive | ||
| computed properties of instances that are otherwise effectively immutable. | ||
|
|
||
| Example:: | ||
|
|
||
| class DataSet: | ||
| def __init__(self, sequence_of_numbers): | ||
| self.data = sequence_of_numbers | ||
|
|
||
| @cached_property | ||
| def stdev(self): | ||
| return statistics.stdev(self.data) | ||
|
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. self._data |
||
|
|
||
| @cached_property | ||
| def variance(self): | ||
| return statistics.variance(self.data) | ||
|
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. self._data |
||
|
|
||
| .. versionadded:: 3.8 | ||
|
|
||
| .. note:: | ||
|
|
||
| This decorator does not support classes which define ``__slots__``. | ||
|
Contributor
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. I'd word this note slightly differently, as there are multiple ways to end up with instances that don't have a mutable mapping as their Something like:
|
||
|
|
||
|
|
||
| .. function:: cmp_to_key(func) | ||
|
|
||
| Transform an old-style comparison function to a :term:`key function`. Used | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -868,3 +868,32 @@ def _method(*args, **kwargs): | |
| @property | ||
| def __isabstractmethod__(self): | ||
| return getattr(self.func, '__isabstractmethod__', False) | ||
|
|
||
|
|
||
| ################################################################################ | ||
| ### cached_property() - computed once per instance, cached as attribute | ||
| ################################################################################ | ||
|
|
||
| class cached_property: | ||
| def __init__(self, func): | ||
| self.func = func | ||
| self.__doc__ = func.__doc__ | ||
| self.lock = RLock() | ||
|
|
||
| def __get__(self, instance, cls=None): | ||
|
Contributor
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. When
Member
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. AFAIK every call to Regarding the argument name, considering both the Data Model documentation and the HOWTO, we see examples in Python's own documentation of descriptors whose
Contributor
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. I doubt if howto is 100% correct. datamodel doesn't allow Using default for always specified argument confuses me. datamodel uses
Contributor
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. If we need to change recommended argument name -- let's discuss it in another issue.
Member
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. Changed signature of |
||
| if instance is None: | ||
| return self | ||
| attrname = self.func.__name__ | ||
| try: | ||
| cache = instance.__dict__ | ||
| except AttributeError: # objects with __slots__ have no __dict__ | ||
|
Contributor
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. Tweak the comment here along similar lines to the note in the docs |
||
| msg = ( | ||
| f"No '__dict__' attribute on {type(instance).__name__!r} " | ||
| f"instance to cache {attrname!r} property." | ||
| ) | ||
| raise TypeError(msg) from None | ||
| with self.lock: | ||
|
Contributor
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.
Member
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. Sure, will update. Thanks for the review!
Contributor
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. looks good |
||
| # check if another thread filled cache while we awaited lock | ||
| if attrname not in cache: | ||
| cache[attrname] = self.func(instance) | ||
| return cache[attrname] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2313,5 +2313,89 @@ def f(*args): | |
| with self.assertRaisesRegex(TypeError, msg): | ||
| f() | ||
|
|
||
| class CachedCostItem: | ||
| _cost = 1 | ||
|
|
||
| def __init__(self): | ||
| self.lock = py_functools.RLock() | ||
|
|
||
| @py_functools.cached_property | ||
| def cost(self): | ||
| """The cost of the item.""" | ||
| with self.lock: | ||
| self._cost += 1 | ||
| return self._cost | ||
|
|
||
|
|
||
| class CachedCostItemWait: | ||
| _cost = 1 | ||
|
|
||
| def __init__(self, event): | ||
| self._cost = 1 | ||
| self.lock = py_functools.RLock() | ||
| self.event = event | ||
|
|
||
| @py_functools.cached_property | ||
| def cost(self): | ||
| self.event.wait(1) | ||
| with self.lock: | ||
| self._cost += 1 | ||
| return self._cost | ||
|
|
||
|
|
||
| class CachedCostItemWithSlots: | ||
| __slots__ = ('_cost') | ||
|
|
||
| def __init__(self): | ||
| self._cost = 1 | ||
|
|
||
| @py_functools.cached_property | ||
| def cost(self): | ||
| """The cost of the item.""" | ||
| return self._cost | ||
|
Contributor
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. Looks like the method is never called
Member
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. Yes, that’s expected. The point of this class is just to test the error message; cached property on classes with slots is not supported. Would you find it clearer if I used
Contributor
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.
|
||
|
|
||
|
|
||
| class TestCachedProperty(unittest.TestCase): | ||
| def test_cached(self): | ||
| item = CachedCostItem() | ||
| self.assertEqual(item.cost, 2) | ||
| self.assertEqual(item.cost, 2) # not 3 | ||
|
|
||
| def test_threaded(self): | ||
| go = threading.Event() | ||
| item = CachedCostItemWait(go) | ||
|
|
||
| num_threads = 3 | ||
|
|
||
| orig_si = sys.getswitchinterval() | ||
| sys.setswitchinterval(1e-6) | ||
| try: | ||
| threads = [ | ||
| threading.Thread(target=lambda: item.cost) | ||
| for k in range(num_threads) | ||
| ] | ||
| with support.start_threads(threads): | ||
| go.set() | ||
| finally: | ||
| sys.setswitchinterval(orig_si) | ||
|
|
||
| self.assertEqual(item.cost, 2) | ||
|
|
||
|
|
||
|
Contributor
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. Redundant line? |
||
| def test_object_with_slots(self): | ||
| item = CachedCostItemWithSlots() | ||
| with self.assertRaisesRegex( | ||
| TypeError, | ||
| "No '__dict__' attribute on 'CachedCostItemWithSlots' instance to cache 'cost' property.", | ||
| ): | ||
| item.cost | ||
|
|
||
| def test_access_from_class(self): | ||
| self.assertIsInstance(CachedCostItem.cost, py_functools.cached_property) | ||
|
|
||
| def test_doc(self): | ||
| self.assertEqual(CachedCostItem.cost.__doc__, "The cost of the item.") | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| unittest.main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| Add ``functools.cached_property`` decorator, for computed properties cached | ||
| for the life of the instance. |
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.
I prefer
_datato implydataset.data = newdatais not supported.