Repository navigation
Fraction wrongfully gets casted into float when given as argument to __rpow__ #119189
Description
Activity
- addedtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
on May 19, 2024 I'm investigating this.
Reproduced under Linux (Ubuntu). I am working on a Fix (I'm in the sprint workshop)
It's a mistake in the implementation of
Fraction.__pow__; rather than returningNotImplementedwhen the type of the exponent is unrecognized and letting the other type handle it through__rpow__normally, it intentionally doesreturn float(a) ** b(whereais the name given toself).It does something similar when a
Fractionis raised to the power of another non-one denominatorRational(return float(a) ** float(b)), but it at least makes more sense there (the comments note that raising a fraction to a fraction generally produces an irrational number, which can't be represented as a fraction in the first place).Is anyone working on a fix? If not, this is an easy one, I can do it.
I worry slightly about possible breakage of existing code, but:
- The existing behavior is a clear break from the operator overloading rules, and not one justified by anything special about fractions (unlike the fraction to fractional power case)
- The existing behavior loses data, while the fix would not, so if the existing behavior is desired, an
__rpow__provider can always revert to the old behavior manually (they don't even need it to be version-specific; if they detect aFraction, which can't be given right now, they convert tofloat; on older Python, they'll never see aFractionin the first place) - I can't imagine many cases where this code path is exercised; if you're writing a type designed to work with numbers like this, it's almost always going to be part of the numeric tower, and should be able to handle other layers of the numeric tower appropriately.
@MojoVampire I am working on a fix I'm in the cpython sprint.
@zitterbewegung: Same. I'm the guy standing up cause my back is borked. Fix is pretty easy:
elif isinstance(b, (float, complex)): return float(a) ** b else: return NotImplementedreplacing the existing final line. Tests are always the pain. I'll review when you're done.
- I can't imagine many cases where this code path is exercised; if you're writing a type designed to work with numbers like this, it's almost always going to be part of the numeric tower, and should be able to handle other layers of the numeric tower appropriately.
For context: I stumbled over the bug when implementing a wrapper number class for exact precision arithmetic that holds the data as a symbolic expression (so e.g. irrational numbers can be used in calculations without error). The class is designed to work with all builtin number types, e.g.
a = Fraction(1, 3) b = ExactNumber(Fraction(1, 2)) print(a**b)
ExactNumber(1/3**1/2)My class is built on the sympy package that does similar things on its own, so this should also be relevant to them.
Anyway: Thanks for the quick response, I really appreciate it.Best,
FabianI worked on this today. The issue itself may be not difficult, but I noticed that tests do not cover many cases in mixed Fraction arithmetic. #119236 adds many new tests, so we will see what other side effect our changes can cause.
@serhiy-storchaka I will look at this with @MojoVampire to see if there are any additional issues.
I'd suggest the news entry simplify to something like:
fractions.Fraction.__pow__no longer coerces itself tofloatfor unrecognized exponent types, instead returningNotImplementedto defer to the exponent's__rpow__as normal.Keeps it focused on what was fixed.
With this fix I see a test failure when manually putting in the new test_fractions code in
#119236======================================================================
FAIL: testMixedPower (test.test_fractions.FractionTest.testMixedPower)Traceback (most recent call last):
File "/Users/r2q2/Projects/cpython/Lib/test/test_fractions.py", line 903, in testMixedPower
self.assertTypedEquals(F(3, 2) ** Rect(2, 0), Polar(2.25, 0.0))
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/r2q2/Projects/cpython/Lib/test/test_fractions.py", line 282, in assertTypedEquals
self.assertEqual(expected, actual)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
AssertionError: Polar(Fraction(9, 4), 0.0) != Polar(2.25, 0.0)
Ran 45 tests in 0.038s
FAILED (failures=1)
test test_fractions failed
test_fractions failed (1 failure)== Tests result: FAILURE ==
1 test failed:
test_fractionsTotal duration: 97 ms
Total tests: run=45 failures=1
Total test files: run=1/1 failed=1
Result: FAILURELooks like a mistake in the test (or if you prefer, a test tailored to the old behavior). The
Rectdefines:def __rpow__(self, other): return Polar(other ** self.x, math.log(other) * self.y)and the test expects
other ** self.xto produce afloat, but givenotheris aFraction, andself.xis anint(whichFractioncan use losslessly to produce a newFraction), the observed result is correct, post-patch.7 remaining items
- added a commit that references this issue
on May 21, 2024 The new tests were purposed to fail with this change. They were added so that we can see what effect the change has and decide whether it is correct. If some visible changes are not correct, we should find other fixes for such cases.
I have other changes for mixed Fraction arithmetic, but I am not sure that they should be backported, so I'll create a separate PR.
- added a commit that references this issue
on May 22, 2024 See also #119838.
Bug report
Bug description:
When using the
**operator with a Fraction as a base and an object that implements__rpow__as an exponent the fraction gets wrongfully casted into a float before being passed to__rpow__Expected Output
Actual Output:
Tested with Python 3.12.3
CPython versions tested on:
3.12
Operating systems tested on:
macOS
Linked PRs