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
gh-87634: deprecate cached_property locking, add lock kwarg #98123
base: main
Are you sure you want to change the base?
Conversation
carljm
commented
Oct 9, 2022
•
edited by bedevere-bot
edited by bedevere-bot
- Issue: functools.cached_property incorrectly locks the entire descriptor on class instead of per-instance locking #87634
|
This causes at least one deprecation warning to be emitted in the stdlib: C:\Users\alexw\coding\cpython>python -m idlelib
Running Debug|x64 interpreter...
C:\Users\alexw\coding\cpython\Lib\platform.py:850: PendingDeprecationWarning: Locking cached_property is deprecated; use @cached_property(lock=False)
@functools.cached_property
C:\Users\alexw\coding\cpython\Lib\platform.py:850: PendingDeprecationWarning: Locking cached_property is deprecated; use @cached_property(lock=False)
@functools.cached_property(Edit: looks like that's already been caught by one of the failing tests in CI :) |
Lib/functools.py
Outdated
| @@ -20,6 +20,7 @@ | |||
| from reprlib import recursive_repr | |||
| from _thread import RLock | |||
| from types import GenericAlias | |||
| from warnings import warn | |||
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.
Should import this inside the if lock: block in cached_property.__init__, so that users only pay the performance cost for importing the module if they're using the deprecated behaviour
| warn("Locking cached_property is deprecated; use @cached_property(lock=False)", | ||
| PendingDeprecationWarning, 2) |
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.
Maybe use warnings._deprecated?
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 don't think warnings._deprecated works here, because it assumes the thing that is deprecated is a single method/function/class with a name. What would you pass in for the name argument of warnings._deprecated in this case?
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.
You could do this:
diff --git a/Lib/functools.py b/Lib/functools.py
index 8ad150d853..5eb772e6b4 100644
--- a/Lib/functools.py
+++ b/Lib/functools.py
@@ -969,9 +969,12 @@ def __init__(self, func=None, *, lock=True):
if func is not None:
self.__doc__ = func.__doc__
if lock:
- from warnings import warn
- warn("Locking cached_property is deprecated; use @cached_property(lock=False)",
- PendingDeprecationWarning, 2)
+ import warnings
+ warnings._deprecated(
+ "lock=True default behaviour of cached_property",
+ "Locking cached_property is deprecated; use @cached_property(lock=False)",
+ remove=(3, 14)
+ )
self.lock = RLock() if lock else NoneAnd then you'd get this behaviour:
C:\Users\alexw\coding\cpython>python
Running Debug|x64 interpreter...
Python 3.12.0a0 (main, Oct 9 2022, 08:02:35) [MSC v.1932 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> # Behaviour on 3.12:
>>>
>>> import functools
>>> x = functools.cached_property(lambda self: 42)
<stdin>:1: DeprecationWarning: Locking cached_property is deprecated; use @cached_property(lock=False)
>>>
>>> # Behaviour when we get to 3.14 beta if the default hasn't been changed yet:
>>>
>>> import sys
>>> sys.version_info = 3, 14, 0, "beta"
>>> del sys.modules["warnings"]; del sys.modules["functools"]
>>> import functools
>>> x = functools.cached_property(lambda self: 42)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Users\alexw\coding\cpython\Lib\functools.py", line 973, in __init__
warnings._deprecated(
File "C:\Users\alexw\coding\cpython\Lib\warnings.py", line 511, in _deprecated
raise RuntimeError(msg)
RuntimeError: 'lock=True default behaviour of cached_property' was slated for removal after Python 3.14 alphaI agree that this probably isn't the use case warnings._deprecated was designed for... but it seems to work pretty well? ;)
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.
Also, any reason you're using PendingDeprecationWarning instead of DeprecationWarning, btw?
Lib/functools.py
Outdated
|
|
||
| def __call__(self, func=None): | ||
| if self.func is not None: | ||
| raise TypeError("'cached_property' object is not callable") |
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.
| raise TypeError("'cached_property' object is not callable") | |
| raise TypeError(f"{type(self).__name__!r} object is not callable") |
Doc/library/functools.rst
Outdated
| cached property would execute only once per instance, but it also prevents | ||
| concurrent execution on different instances of the same class, causing | ||
| unnecessary and excessive serialization of updates. This locking behavior | ||
| is deprecated and can be avoided by passing ``lock=False``:: |
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 deprecated and can be avoided by passing ``lock=False``:: | |
| is deprecated, and will be removed in Python 3.14. It can be avoided by | |
| passing ``lock=False``:: |
| .. versionadded:: 3.8 | ||
|
|
||
| .. versionchanged:: 3.12 | ||
| The ``lock`` argument was added. | ||
|
|
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.
| .. deprecated-removed:: 3.12 3.14 | |
| The locking behaviour of ``cached_property`` is deprecated, and will be | |
| removed in 3.14. Use ``lock=False`` to avoid the deprecated behaviour. |
| @@ -245,6 +245,17 @@ Deprecated | |||
| :exc:`ImportWarning`). | |||
| (Contributed by Brett Cannon in :gh:`65961`.) | |||
|
|
|||
| * The locking behavior of :class:`~functools.cached_property` is deprecated. | |||
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.
| * The locking behavior of :class:`~functools.cached_property` is deprecated. | |
| * The locking behavior of :class:`~functools.cached_property` is deprecated, | |
| and will be removed in Python 3.14. |
|
Ripping out the locking logic seems reasonable — as you say, it was a mistake to include it in the first place. That said, this PR's approach to deprecation is going to be rough on users and will leave a permanent mark on the API. ISTM the most graceful thing we could do is to just fix the locking logic. I put some effort into this but it required more deep thought than I had time for. It might be a good sprint topic. |
I agree that this would be the best thing to do, if we can figure out how to do it. The fallout from this PR just on the stdlib and the CPython test suite is pretty concerning. And if users want to write Python code that's compatible on both Python 3.11 and Python 3.12, while avoiding import sys
if sys.version_info >= (3, 12):
import functools
cached_property = functools.partial(functools.cached_property, lock=False)
else:
from functools import cached_property(If we do decide to go with the approach proposed in this PR, it might be worth adding the above snippet to the "What's new in 3.12" entry.) |
|
Also, if locking is going to be "left to the users", there should be an example of how to make sure a call doesn't get made more than once. The current way can be catastrophically inefficient but at least it isn't wrong. |