Description
Bug report
This comes from a SymPy issue: sympy/sympy#23774
It looks like there is a nondeterministic error in the optimisation introduced in 96346cb from #27722 first included in CPython version 3.11.0a1. The optimisation speeds up calling methods but sometimes retrieves the wrong attribute from a class whose metaclass defines a property method.
The way that this arises is quite sensitive to small changes so I haven't been able to distil a standalone reproducer. I'll show how to reproduce this using SymPy below but first this is a simplified schematic of the situation:
class MetaA(type): def __init__(cls, *args, **kws): pass class A(metaclass=MetaA): def method(self, rule): return 'A method' class MetaB(MetaA): @property def method(self): def method_inner(rule): return 'MetaB function' return method_inner class B(A, metaclass=MetaB): pass print(B.method(1)) # MetaB function print(B().method(1)) # A method
Here B
is a subclass of A
but an instance of MetaB
. Both define method
but the MetaB
method should be used when accessed from the class B
rather than an instance B()
. The failure seen in SymPy is that sometimes B.method(1)
will execute as A.method(1)
which fails because of the missing self
argument.
The following code reproduces the problem and fails something like 50% of the time with SymPy 1.10.1 and CPython 3.11.0a1-3.11.0b4:
from sympy import * x, y = symbols('x, y') f = Function('f') # These two lines look irrelevant but are needed: expr = sin(x*exp(y)) Derivative(expr, y).subs(y, x).doit() expr = Subs(Derivative(f(f(x)), x), f, cos) # This is where it blows up: expr.doit()
The failure is not deterministic:
$ python bug.py $ python bug.py $ python bug.py Traceback (most recent call last): File "/home/oscar/current/sympy/sympy.git/bug.py", line 13, in <module> expr.doit() ^^^^^^^^^^^ File "/home/oscar/current/sympy/sympy.git/sympy/core/function.py", line 2246, in doit e = e.subs(vi, p[i]) ^^^^^^^^^^^^^^^^ File "/home/oscar/current/sympy/sympy.git/sympy/core/basic.py", line 997, in subs rv = rv._subs(old, new, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/oscar/current/sympy/sympy.git/sympy/core/cache.py", line 70, in wrapper retval = cfunc(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^ File "/home/oscar/current/sympy/sympy.git/sympy/core/basic.py", line 1109, in _subs rv = self._eval_subs(old, new) ^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/oscar/current/sympy/sympy.git/sympy/core/function.py", line 1737, in _eval_subs nfree = new.xreplace(syms).free_symbols ^^^^^^^^^^^^^^^^^^ TypeError: Basic.xreplace() missing 1 required positional argument: 'rule'
The failure can be reproduced deterministically by setting the hash seed:
$ PYTHONHASHSEED=1 python bug.py ... TypeError: Basic.xreplace() missing 1 required positional argument: 'rule'
In the debugger the same code that already failed succeeds:
$ PYTHONHASHSEED=1 python -m pdb bug.py > /home/oscar/current/sympy/sympy.git/bug.py(1)<module>() -> from sympy import * (Pdb) c Traceback (most recent call last): File "/media/oscar/EXT4_STUFF/src/cpython/Lib/pdb.py", line 1768, in main pdb._run(target) File "/media/oscar/EXT4_STUFF/src/cpython/Lib/pdb.py", line 1646, in _run self.run(target.code) File "/media/oscar/EXT4_STUFF/src/cpython/Lib/bdb.py", line 597, in run exec(cmd, globals, locals) File "<string>", line 1, in <module> File "/home/oscar/current/sympy/sympy.git/bug.py", line 13, in <module> expr.doit() File "/home/oscar/current/sympy/sympy.git/sympy/core/function.py", line 2255, in doit e = e.subs(vi, p[i]) ^^^^^^^^^^^^^^^^ File "/home/oscar/current/sympy/sympy.git/sympy/core/basic.py", line 993, in subs rv = rv._subs(old, new, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/oscar/current/sympy/sympy.git/sympy/core/cache.py", line 70, in wrapper retval = cfunc(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^ File "/home/oscar/current/sympy/sympy.git/sympy/core/basic.py", line 1105, in _subs rv = self._eval_subs(old, new) ^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/oscar/current/sympy/sympy.git/sympy/core/function.py", line 1746, in _eval_subs nfree = new.xreplace(syms).free_symbols ^^^^^^^^^^^^^^^^^^ TypeError: Basic.xreplace() missing 1 required positional argument: 'rule' Uncaught exception. Entering post mortem debugging Running 'cont' or 'step' will restart the program > /home/oscar/current/sympy/sympy.git/sympy/core/function.py(1746)_eval_subs() -> nfree = new.xreplace(syms).free_symbols (Pdb) p new.xreplace <function FunctionClass.xreplace.<locals>.<lambda> at 0x7f0dd0f763e0> (Pdb) p new.xreplace(syms) cos (Pdb) p new.xreplace(syms).free_symbols set()
The actual arrangement of SymPy classes is something like this:
class ManagedProperties(type): def __init__(cls, *args, **kws): pass class Basic(metaclass=ManagedProperties): def xreplace(self, rule): print('Basic') class Expr(Basic): pass class FunctionClass(ManagedProperties): @property def xreplace(self): return lambda rule: print('functionclass') class Application(Basic, metaclass=FunctionClass): pass class Function(Application, Expr): pass class cos(Function): pass cos.xreplace(1)
Your environment
- CPython versions tested on: 3.11.0a1-3.11.0b4 (3.10.5 or lower does not have the bug)
- Operating system and architecture: Ubuntu x86-64.
Metadata
Metadata
Assignees
Labels
Projects
Status