Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 85424c9

Browse files
committed
Issue #6431: Fix Fraction comparisons to return NotImplemented when
the Fraction type doesn't know how to handle the comparison without loss of accuracy. Also, make sure that comparisons between Fractions and float infinities or nans do the right thing.
1 parent 8c1a50a commit 85424c9

3 files changed

Lines changed: 162 additions & 30 deletions

File tree

Lib/fractions.py

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -501,54 +501,56 @@ def __eq__(a, b):
501501
if isinstance(b, numbers.Complex) and b.imag == 0:
502502
b = b.real
503503
if isinstance(b, float):
504-
return a == a.from_float(b)
504+
if math.isnan(b) or math.isinf(b):
505+
# comparisons with an infinity or nan should behave in
506+
# the same way for any finite a, so treat a as zero.
507+
return 0.0 == b
508+
else:
509+
return a == a.from_float(b)
505510
else:
506-
# XXX: If b.__eq__ is implemented like this method, it may
507-
# give the wrong answer after float(a) changes a's
508-
# value. Better ways of doing this are welcome.
509-
return float(a) == b
511+
# Since a doesn't know how to compare with b, let's give b
512+
# a chance to compare itself with a.
513+
return NotImplemented
510514

511-
def _subtractAndCompareToZero(a, b, op):
512-
"""Helper function for comparison operators.
515+
def _richcmp(self, other, op):
516+
"""Helper for comparison operators, for internal use only.
513517
514-
Subtracts b from a, exactly if possible, and compares the
515-
result with 0 using op, in such a way that the comparison
516-
won't recurse. If the difference raises a TypeError, returns
517-
NotImplemented instead.
518+
Implement comparison between a Rational instance `self`, and
519+
either another Rational instance or a float `other`. If
520+
`other` is not a Rational instance or a float, return
521+
NotImplemented. `op` should be one of the six standard
522+
comparison operators.
518523
519524
"""
520-
if isinstance(b, numbers.Complex) and b.imag == 0:
521-
b = b.real
522-
if isinstance(b, float):
523-
b = a.from_float(b)
524-
try:
525-
# XXX: If b <: Real but not <: Rational, this is likely
526-
# to fall back to a float. If the actual values differ by
527-
# less than MIN_FLOAT, this could falsely call them equal,
528-
# which would make <= inconsistent with ==. Better ways of
529-
# doing this are welcome.
530-
diff = a - b
531-
except TypeError:
525+
# convert other to a Rational instance where reasonable.
526+
if isinstance(other, numbers.Rational):
527+
return op(self._numerator * other.denominator,
528+
self._denominator * other.numerator)
529+
if isinstance(other, numbers.Complex) and other.imag == 0:
530+
other = other.real
531+
if isinstance(other, float):
532+
if math.isnan(other) or math.isinf(other):
533+
return op(0.0, other)
534+
else:
535+
return op(self, self.from_float(other))
536+
else:
532537
return NotImplemented
533-
if isinstance(diff, numbers.Rational):
534-
return op(diff.numerator, 0)
535-
return op(diff, 0)
536538

537539
def __lt__(a, b):
538540
"""a < b"""
539-
return a._subtractAndCompareToZero(b, operator.lt)
541+
return a._richcmp(b, operator.lt)
540542

541543
def __gt__(a, b):
542544
"""a > b"""
543-
return a._subtractAndCompareToZero(b, operator.gt)
545+
return a._richcmp(b, operator.gt)
544546

545547
def __le__(a, b):
546548
"""a <= b"""
547-
return a._subtractAndCompareToZero(b, operator.le)
549+
return a._richcmp(b, operator.le)
548550

549551
def __ge__(a, b):
550552
"""a >= b"""
551-
return a._subtractAndCompareToZero(b, operator.ge)
553+
return a._richcmp(b, operator.ge)
552554

553555
def __bool__(a):
554556
"""a != 0"""

Lib/test/test_fractions.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from decimal import Decimal
44
from test.support import run_unittest
55
import math
6+
import numbers
67
import operator
78
import fractions
89
import unittest
@@ -11,6 +12,69 @@
1112
F = fractions.Fraction
1213
gcd = fractions.gcd
1314

15+
class DummyFloat(object):
16+
"""Dummy float class for testing comparisons with Fractions"""
17+
18+
def __init__(self, value):
19+
if not isinstance(value, float):
20+
raise TypeError("DummyFloat can only be initialized from float")
21+
self.value = value
22+
23+
def _richcmp(self, other, op):
24+
if isinstance(other, numbers.Rational):
25+
return op(F.from_float(self.value), other)
26+
elif isinstance(other, DummyFloat):
27+
return op(self.value, other.value)
28+
else:
29+
return NotImplemented
30+
31+
def __eq__(self, other): return self._richcmp(other, operator.eq)
32+
def __le__(self, other): return self._richcmp(other, operator.le)
33+
def __lt__(self, other): return self._richcmp(other, operator.lt)
34+
def __ge__(self, other): return self._richcmp(other, operator.ge)
35+
def __gt__(self, other): return self._richcmp(other, operator.gt)
36+
37+
# shouldn't be calling __float__ at all when doing comparisons
38+
def __float__(self):
39+
assert False, "__float__ should not be invoked for comparisons"
40+
41+
# same goes for subtraction
42+
def __sub__(self, other):
43+
assert False, "__sub__ should not be invoked for comparisons"
44+
__rsub__ = __sub__
45+
46+
47+
class DummyRational(object):
48+
"""Test comparison of Fraction with a naive rational implementation."""
49+
50+
def __init__(self, num, den):
51+
g = gcd(num, den)
52+
self.num = num // g
53+
self.den = den // g
54+
55+
def __eq__(self, other):
56+
if isinstance(other, fractions.Fraction):
57+
return (self.num == other._numerator and
58+
self.den == other._denominator)
59+
else:
60+
return NotImplemented
61+
62+
def __lt__(self, other):
63+
return(self.num * other._denominator < self.den * other._numerator)
64+
65+
def __gt__(self, other):
66+
return(self.num * other._denominator > self.den * other._numerator)
67+
68+
def __le__(self, other):
69+
return(self.num * other._denominator <= self.den * other._numerator)
70+
71+
def __ge__(self, other):
72+
return(self.num * other._denominator >= self.den * other._numerator)
73+
74+
# this class is for testing comparisons; conversion to float
75+
# should never be used for a comparison, since it loses accuracy
76+
def __float__(self):
77+
assert False, "__float__ should not be invoked"
1478

1579
class GcdTest(unittest.TestCase):
1680

@@ -324,6 +388,50 @@ def testComparisons(self):
324388
self.assertFalse(F(1, 2) != F(1, 2))
325389
self.assertTrue(F(1, 2) != F(1, 3))
326390

391+
def testComparisonsDummyRational(self):
392+
self.assertTrue(F(1, 2) == DummyRational(1, 2))
393+
self.assertTrue(DummyRational(1, 2) == F(1, 2))
394+
self.assertFalse(F(1, 2) == DummyRational(3, 4))
395+
self.assertFalse(DummyRational(3, 4) == F(1, 2))
396+
397+
self.assertTrue(F(1, 2) < DummyRational(3, 4))
398+
self.assertFalse(F(1, 2) < DummyRational(1, 2))
399+
self.assertFalse(F(1, 2) < DummyRational(1, 7))
400+
self.assertFalse(F(1, 2) > DummyRational(3, 4))
401+
self.assertFalse(F(1, 2) > DummyRational(1, 2))
402+
self.assertTrue(F(1, 2) > DummyRational(1, 7))
403+
self.assertTrue(F(1, 2) <= DummyRational(3, 4))
404+
self.assertTrue(F(1, 2) <= DummyRational(1, 2))
405+
self.assertFalse(F(1, 2) <= DummyRational(1, 7))
406+
self.assertFalse(F(1, 2) >= DummyRational(3, 4))
407+
self.assertTrue(F(1, 2) >= DummyRational(1, 2))
408+
self.assertTrue(F(1, 2) >= DummyRational(1, 7))
409+
410+
self.assertTrue(DummyRational(1, 2) < F(3, 4))
411+
self.assertFalse(DummyRational(1, 2) < F(1, 2))
412+
self.assertFalse(DummyRational(1, 2) < F(1, 7))
413+
self.assertFalse(DummyRational(1, 2) > F(3, 4))
414+
self.assertFalse(DummyRational(1, 2) > F(1, 2))
415+
self.assertTrue(DummyRational(1, 2) > F(1, 7))
416+
self.assertTrue(DummyRational(1, 2) <= F(3, 4))
417+
self.assertTrue(DummyRational(1, 2) <= F(1, 2))
418+
self.assertFalse(DummyRational(1, 2) <= F(1, 7))
419+
self.assertFalse(DummyRational(1, 2) >= F(3, 4))
420+
self.assertTrue(DummyRational(1, 2) >= F(1, 2))
421+
self.assertTrue(DummyRational(1, 2) >= F(1, 7))
422+
423+
def testComparisonsDummyFloat(self):
424+
x = DummyFloat(1./3.)
425+
y = F(1, 3)
426+
self.assertTrue(x != y)
427+
self.assertTrue(x < y or x > y)
428+
self.assertFalse(x == y)
429+
self.assertFalse(x <= y and x >= y)
430+
self.assertTrue(y != x)
431+
self.assertTrue(y < x or y > x)
432+
self.assertFalse(y == x)
433+
self.assertFalse(y <= x and y >= x)
434+
327435
def testMixedLess(self):
328436
self.assertTrue(2 < F(5, 2))
329437
self.assertFalse(2 < F(4, 2))
@@ -335,6 +443,13 @@ def testMixedLess(self):
335443
self.assertTrue(0.4 < F(1, 2))
336444
self.assertFalse(0.5 < F(1, 2))
337445

446+
self.assertFalse(float('inf') < F(1, 2))
447+
self.assertTrue(float('-inf') < F(0, 10))
448+
self.assertFalse(float('nan') < F(-3, 7))
449+
self.assertTrue(F(1, 2) < float('inf'))
450+
self.assertFalse(F(17, 12) < float('-inf'))
451+
self.assertFalse(F(144, -89) < float('nan'))
452+
338453
def testMixedLessEqual(self):
339454
self.assertTrue(0.5 <= F(1, 2))
340455
self.assertFalse(0.6 <= F(1, 2))
@@ -345,6 +460,13 @@ def testMixedLessEqual(self):
345460
self.assertTrue(F(4, 2) <= 2)
346461
self.assertFalse(F(5, 2) <= 2)
347462

463+
self.assertFalse(float('inf') <= F(1, 2))
464+
self.assertTrue(float('-inf') <= F(0, 10))
465+
self.assertFalse(float('nan') <= F(-3, 7))
466+
self.assertTrue(F(1, 2) <= float('inf'))
467+
self.assertFalse(F(17, 12) <= float('-inf'))
468+
self.assertFalse(F(144, -89) <= float('nan'))
469+
348470
def testBigFloatComparisons(self):
349471
# Because 10**23 can't be represented exactly as a float:
350472
self.assertFalse(F(10**23) == float(10**23))
@@ -369,6 +491,10 @@ def testMixedEqual(self):
369491
self.assertFalse(2 == F(3, 2))
370492
self.assertTrue(F(4, 2) == 2)
371493
self.assertFalse(F(5, 2) == 2)
494+
self.assertFalse(F(5, 2) == float('nan'))
495+
self.assertFalse(float('nan') == F(3, 7))
496+
self.assertFalse(F(5, 2) == float('inf'))
497+
self.assertFalse(float('-inf') == F(2, 5))
372498

373499
def testStringification(self):
374500
self.assertEquals("Fraction(7, 3)", repr(F(7, 3)))

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ C-API
4040
Library
4141
-------
4242

43+
- Issue #6431: Make Fraction type return NotImplemented when it doesn't
44+
know how to handle a comparison without loss of precision. Also add
45+
correct handling of infinities and nans for comparisons with float.
46+
4347
- Issue #6415: Fixed warnings.warn sagfault on bad formatted string.
4448

4549
- Issue #6358: The exit status of a command started with os.popen() was

0 commit comments

Comments
 (0)