-
-
Notifications
You must be signed in to change notification settings - Fork 32.1k
performance shortcut in functools.partial behaves differently in C and in Python version #100242
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
Comments
Note that I needed to modify the tests which assume the c-import worked: # HG changeset patch
# Branch py3.9
# Node ID 4016ae64db38512857aea9987146e897f6b25ebd
# Parent f5d3e9e0fa5cb1ee62ccd2867bcf5fe094d4eae8
fix tests for no c_functools
diff -r f5d3e9e0fa5c -r 4016ae64db38 lib-python/3/test/test_functools.py
--- a/lib-python/3/test/test_functools.py Fri Dec 02 16:15:06 2022 +0200
+++ b/lib-python/3/test/test_functools.py Sat Dec 03 10:42:01 2022 +0200
@@ -199,7 +199,9 @@
kwargs = {'a': object(), 'b': object()}
kwargs_reprs = ['a={a!r}, b={b!r}'.format_map(kwargs),
'b={b!r}, a={a!r}'.format_map(kwargs)]
- if self.partial in (c_functools.partial, py_functools.partial):
+ if c_functools and self.partial is c_functools.partial:
+ name = 'functools.partial'
+ elif self.partial is py_functools.partial:
name = 'functools.partial'
else:
name = self.partial.__name__
@@ -221,7 +223,9 @@
for kwargs_repr in kwargs_reprs])
def test_recursive_repr(self):
- if self.partial in (c_functools.partial, py_functools.partial):
+ if c_functools and self.partial is c_functools.partial:
+ name = 'functools.partial'
+ elif self.partial is py_functools.partial:
name = 'functools.partial'
else:
name = self.partial.__name__
@@ -1753,9 +1757,10 @@
def py_cached_func(x, y):
return 3 * x + y
-@c_functools.lru_cache()
-def c_cached_func(x, y):
- return 3 * x + y
+if c_functools:
+ @c_functools.lru_cache()
+ def c_cached_func(x, y):
+ return 3 * x + y
class TestLRUPy(TestLRU, unittest.TestCase):
@@ -1772,18 +1777,20 @@
return 3 * x + y
-class TestLRUC(TestLRU, unittest.TestCase):
- module = c_functools
- cached_func = c_cached_func,
-
- @module.lru_cache()
- def cached_meth(self, x, y):
- return 3 * x + y
-
- @staticmethod
- @module.lru_cache()
- def cached_staticmeth(x, y):
- return 3 * x + y
+if c_functools:
+ @unittest.skipUnless(c_functools, 'requires the C _functools module')
+ class TestLRUC(TestLRU, unittest.TestCase):
+ module = c_functools
+ cached_func = c_cached_func,
+
+ @module.lru_cache()
+ def cached_meth(self, x, y):
+ return 3 * x + y
+
+ @staticmethod
+ @module.lru_cache()
+ def cached_staticmeth(x, y):
+ return 3 * x + y
class TestSingleDispatch(unittest.TestCase): |
…h C code (GH-100244) in partial.__new__, before checking for the existence of the attribute 'func', first check whether the argument is an instance of partial.
Should this issue be closed? It looks like there's no plan to backport this to 3.12 |
Yeah, I'm happy with the current situation, I'm closing it. |
So changes are only going to be applied in Python 3.13? Isn't it considered a bug/a subject to include in a bugfix release? Just curious, since the commit 5a0209f only has a v3.13.0b1 tag and I don't know how to inspect it otherwise. In my definitely-real-world-not-an-artificial script:plant("flower") Prints plant_a_flower = functools.partial(plant, "flower")
plant_a_flower() suddenly prints def nuke(thing):
print("Planting nuclear bomb to destroy the", thing)
def plant(thing):
print("Planting a", thing)
plant.func = nuke # func = Flower UNdo Command, use very carefully
plant.args = ()
plant.keywords = dict()
plant("flower")
plant_a_flower = functools.partial(plant, "flower")
plant_a_flower() # expecting the same output as plant("flower") |
Uh oh!
There was an error while loading. Please reload this page.
Bug
functools.partial
is implemented infunctools.py
and in_functoolsmodule.c
. The former is almost never used, so libraries come to depend on the quirks and corner cases of the C implementation. This is a problem for PyPy, where the Python implementation is the only one as of the most recent PyPy version. Here's one such difference, which was uncovered by thelxml
library. The following code leads to aRecursionError
:this is the traceback:
The problem is the following performance shortcut in
partial.__new__
:Basically in this case
func
is an object where callinghasattr(func, "func")
is not safe. The equivalent C code does this check:In particular, it does not simply call
hasattr
onfunc
.Real World Version
This is not an artificial problem, we discovered this via the class
lxml.builder.ElementMaker
. It has a__call__
method implemented. It also has__getattr__
that looks like this:Which yields the above
RecursionError
on PyPy.Solution ideas
One approach would be to file a bug with
lxml
, but it is likely that more libraries depend on this behaviour. So I would suggest to change the__new__
Python code to add anisinstance
check, to bring its behaviour closer to that of the C code:I'll open a PR with this approach soon. /cc @mgorny
Linked PRs
The text was updated successfully, but these errors were encountered: