diff --git a/Doc/library/fractions.rst b/Doc/library/fractions.rst index 58e7126b0bf212..4e9ce9bcfcdf18 100644 --- a/Doc/library/fractions.rst +++ b/Doc/library/fractions.rst @@ -14,27 +14,25 @@ The :mod:`fractions` module provides support for rational number arithmetic. -A Fraction instance can be constructed from a pair of integers, from -another rational number, or from a string. +A Fraction instance can be constructed from a numerator and denominator +or from a string. .. class:: Fraction(numerator=0, denominator=1) - Fraction(other_fraction) - Fraction(float) - Fraction(decimal) Fraction(string) - The first version requires that *numerator* and *denominator* are instances - of :class:`numbers.Rational` and returns a new :class:`Fraction` instance - with value ``numerator/denominator``. If *denominator* is :const:`0`, it - raises a :exc:`ZeroDivisionError`. The second version requires that - *other_fraction* is an instance of :class:`numbers.Rational` and returns a - :class:`Fraction` instance with the same value. The next two versions accept - either a :class:`float` or a :class:`decimal.Decimal` instance, and return a - :class:`Fraction` instance with exactly the same value. Note that due to the - usual issues with binary floating-point (see :ref:`tut-fp-issues`), the - argument to ``Fraction(1.1)`` is not exactly equal to 11/10, and so + In the first version, *numerator* and *denominator* must either have + an ``as_integer_ratio()`` method or be instances of + :class:`numbers.Rational`. This includes in particular :class:`int`, + :class:`Fraction`, :class:`float` and :class:`decimal.Decimal`. + It returns a new :class:`Fraction` instance with value + ``numerator/denominator``. If *denominator* is :const:`0`, it raises a + :exc:`ZeroDivisionError`. + + Note that due to the usual issues with binary floating-point + (see :ref:`tut-fp-issues`), 1.1 is not exactly equal to 11/10, and so ``Fraction(1.1)`` does *not* return ``Fraction(11, 10)`` as one might expect. (But see the documentation for the :meth:`limit_denominator` method below.) + The last version of the constructor expects a string or unicode instance. The usual form for this instance is:: @@ -69,6 +67,8 @@ another rational number, or from a string. Fraction(9, 4) >>> Fraction(1.1) Fraction(2476979795053773, 2251799813685248) + >>> Fraction(3.5, 2.5) + Fraction(7, 5) >>> from decimal import Decimal >>> Fraction(Decimal('1.1')) Fraction(11, 10) @@ -82,7 +82,12 @@ another rational number, or from a string. .. versionchanged:: 3.2 The :class:`Fraction` constructor now accepts :class:`float` and - :class:`decimal.Decimal` instances. + :class:`decimal.Decimal` instances as a single argument. + + .. versionchanged:: 3.9 + The :class:`Fraction` constructor now accepts any object with + ``as_integer_ratio()`` (in particular also :class:`float` and + :class:`decimal.Decimal` instances) as numerator or denominator. .. attribute:: numerator diff --git a/Lib/fractions.py b/Lib/fractions.py index 2e7047a81844d2..bf5e8206dc770d 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -57,25 +57,43 @@ def _gcd(a, b): """, re.VERBOSE | re.IGNORECASE) +def _as_integer_ratio(obj): + """ + Return a pair (numerator, denominator) if obj has an ``as_integer_ratio`` + method or is an instance of ``numbers.Rational``. Return ``NotImplemented`` + if neither works. + """ + try: + f = obj.as_integer_ratio + except AttributeError: + if isinstance(obj, numbers.Rational): + return (obj.numerator, obj.denominator) + else: + return f() + + return NotImplemented + + class Fraction(numbers.Rational): """This class implements rational numbers. In the two-argument form of the constructor, Fraction(8, 6) will - produce a rational number equivalent to 4/3. Both arguments must - be Rational. The numerator defaults to 0 and the denominator - defaults to 1 so that Fraction(3) == 3 and Fraction() == 0. + produce a rational number equivalent to 4/3. The numerator defaults + to 0 and the denominator defaults to 1 so that Fraction(3) == 3 and + Fraction() == 0. The numerator and denominator can be: + + - objects with an ``as_integer_ratio()`` method (this includes + integers, Fractions, floats and Decimal instances) - Fractions can also be constructed from: + - other Rational instances + + Fractions can also be constructed from a string (in this case, only + a single argument is allowed): - numeric strings similar to those accepted by the float constructor (for example, '-2.3' or '1e10') - strings of the form '123/456' - - - float and Decimal instances - - - other Rational instances (including integers) - """ __slots__ = ('_numerator', '_denominator') @@ -115,23 +133,19 @@ def __new__(cls, numerator=0, denominator=None, *, _normalize=True): self = super(Fraction, cls).__new__(cls) if denominator is None: + # Fast path for Fraction(int) if type(numerator) is int: self._numerator = numerator self._denominator = 1 return self - - elif isinstance(numerator, numbers.Rational): - self._numerator = numerator.numerator - self._denominator = numerator.denominator - return self - - elif isinstance(numerator, (float, Decimal)): - # Exact conversion - self._numerator, self._denominator = numerator.as_integer_ratio() - return self - + nd = _as_integer_ratio(numerator) + if nd is not NotImplemented: + numerator, denominator = nd + # Assume that result is already normalized + _normalize = False + # _as_integer_ratio() returned NotImplemented, so we + # failed unless we got a string elif isinstance(numerator, str): - # Handle construction from strings. m = _RATIONAL_FORMAT.match(numerator) if m is None: raise ValueError('Invalid literal for Fraction: %r' % @@ -158,21 +172,24 @@ def __new__(cls, numerator=0, denominator=None, *, _normalize=True): numerator = -numerator else: - raise TypeError("argument should be a string " - "or a Rational instance") + raise TypeError("argument should be a string, " + "a Rational instance or have " + "an as_integer_ratio() method") elif type(numerator) is int is type(denominator): - pass # *very* normal case - - elif (isinstance(numerator, numbers.Rational) and - isinstance(denominator, numbers.Rational)): - numerator, denominator = ( - numerator.numerator * denominator.denominator, - denominator.numerator * numerator.denominator - ) + # Fast path for Fraction(int, int) + pass else: - raise TypeError("both arguments should be " - "Rational instances") + x = _as_integer_ratio(numerator) + y = _as_integer_ratio(denominator) + if x is NotImplemented or y is NotImplemented: + raise TypeError("both arguments should be " + "a Rational instance or have " + "an as_integer_ratio() method") + num1, den1 = x + den2, num2 = y + numerator = num1 * num2 + denominator = den1 * den2 if denominator == 0: raise ZeroDivisionError('Fraction(%s, 0)' % numerator) diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py index 18ab28cfebe0c8..baf471613e1d86 100644 --- a/Lib/test/test_fractions.py +++ b/Lib/test/test_fractions.py @@ -78,6 +78,15 @@ def __ge__(self, other): def __float__(self): assert False, "__float__ should not be invoked" + +class IntegerRatio: + def __init__(self, num, den): + self.integer_ratio = (num, den) + + def as_integer_ratio(self): + return self.integer_ratio + + class DummyFraction(fractions.Fraction): """Dummy Fraction subclass for copy and deepcopy testing.""" @@ -155,11 +164,13 @@ def testInit(self): self.assertRaises(TypeError, F, "3/2", 3) self.assertRaises(TypeError, F, 3, 0j) self.assertRaises(TypeError, F, 3, 1j) + self.assertRaises(TypeError, F, 1j, 3) self.assertRaises(TypeError, F, 1, 2, 3) @requires_IEEE_754 def testInitFromFloat(self): self.assertEqual((5, 2), _components(F(2.5))) + self.assertEqual((5, 7), _components(F(2.5, 3.5))) self.assertEqual((0, 1), _components(F(-0.0))) self.assertEqual((3602879701896397, 36028797018963968), _components(F(0.1))) @@ -171,6 +182,8 @@ def testInitFromFloat(self): def testInitFromDecimal(self): self.assertEqual((11, 10), _components(F(Decimal('1.1')))) + self.assertEqual((11, 10), + _components(F(Decimal('9.9'), Decimal(9)))) self.assertEqual((7, 200), _components(F(Decimal('3.5e-2')))) self.assertEqual((0, 1), @@ -302,6 +315,14 @@ def testFromDecimal(self): ValueError, "cannot convert NaN to integer ratio", F.from_decimal, Decimal("snan")) + def testFromIntegerRatio(self): + a = IntegerRatio(-5, 4) + b = IntegerRatio(-5, 3) + self.assertEqual(F(a).as_integer_ratio(), (-5, 4)) + self.assertEqual(F(a, b).as_integer_ratio(), (3, 4)) + self.assertEqual(F(1, a).as_integer_ratio(), (-4, 5)) + self.assertEqual(F(a, 5.0).as_integer_ratio(), (-1, 4)) + def test_as_integer_ratio(self): self.assertEqual(F(4, 6).as_integer_ratio(), (2, 3)) self.assertEqual(F(-4, 6).as_integer_ratio(), (-2, 3)) diff --git a/Misc/NEWS.d/next/Library/2019-08-18-22-29-59.bpo-37836.BbvvFm.rst b/Misc/NEWS.d/next/Library/2019-08-18-22-29-59.bpo-37836.BbvvFm.rst new file mode 100644 index 00000000000000..2ef6d2863ea387 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-08-18-22-29-59.bpo-37836.BbvvFm.rst @@ -0,0 +1,3 @@ +When constructing a :class:`fractions.Fraction`, the given numerator and +denominator may now be any object with an ``as_integer_ratio`` method. +This allows, for example, passing floats for both the numerator and denominator.