gh-82017: Support as_integer_ratio() in the Fraction constructor (GH-120271)

Any objects that have the as_integer_ratio() method (e.g. numpy.float128)
can now be converted to a fraction.
This commit is contained in:
Serhiy Storchaka 2024-07-19 08:06:53 +03:00 committed by GitHub
parent eaf094c09b
commit c8d2630995
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 66 additions and 13 deletions

View File

@ -17,25 +17,30 @@ 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.
.. index:: single: as_integer_ratio()
.. class:: Fraction(numerator=0, denominator=1)
Fraction(other_fraction)
Fraction(float)
Fraction(decimal)
Fraction(number)
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 ``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
raises a :exc:`ZeroDivisionError`.
The second version requires that *number* is an instance of
:class:`numbers.Rational` or has the :meth:`!as_integer_ratio` method
(this includes :class:`float` and :class:`decimal.Decimal`).
It returns a :class:`Fraction` instance with exactly the same value.
Assumed, that the :meth:`!as_integer_ratio` method returns a pair
of coprime integers and last one is positive.
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
``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 last version of the constructor expects a string.
The usual form for this instance is::
[sign] numerator ['/' denominator]
@ -110,6 +115,10 @@ another rational number, or from a string.
Formatting of :class:`Fraction` instances without a presentation type
now supports fill, alignment, sign handling, minimum width and grouping.
.. versionchanged:: 3.14
The :class:`Fraction` constructor now accepts any objects with the
:meth:`!as_integer_ratio` method.
.. attribute:: numerator
Numerator of the Fraction in lowest term.

View File

@ -100,6 +100,13 @@ ast
(Contributed by Bénédikt Tran in :gh:`121141`.)
fractions
---------
Added support for converting any objects that have the
:meth:`!as_integer_ratio` method to a :class:`~fractions.Fraction`.
(Contributed by Serhiy Storchaka in :gh:`82017`.)
os
--

View File

@ -3,7 +3,6 @@
"""Fraction, infinite-precision, rational numbers."""
from decimal import Decimal
import functools
import math
import numbers
@ -244,7 +243,9 @@ class Fraction(numbers.Rational):
self._denominator = numerator.denominator
return self
elif isinstance(numerator, (float, Decimal)):
elif (isinstance(numerator, float) or
(not isinstance(numerator, type) and
hasattr(numerator, 'as_integer_ratio'))):
# Exact conversion
self._numerator, self._denominator = numerator.as_integer_ratio()
return self
@ -278,8 +279,7 @@ class Fraction(numbers.Rational):
numerator = -numerator
else:
raise TypeError("argument should be a string "
"or a Rational instance")
raise TypeError("argument should be a string or a number")
elif type(numerator) is int is type(denominator):
pass # *very* normal case

View File

@ -354,6 +354,41 @@ class FractionTest(unittest.TestCase):
self.assertRaises(OverflowError, F, Decimal('inf'))
self.assertRaises(OverflowError, F, Decimal('-inf'))
def testInitFromIntegerRatio(self):
class Ratio:
def __init__(self, ratio):
self._ratio = ratio
def as_integer_ratio(self):
return self._ratio
self.assertEqual((7, 3), _components(F(Ratio((7, 3)))))
errmsg = "argument should be a string or a number"
# the type also has an "as_integer_ratio" attribute.
self.assertRaisesRegex(TypeError, errmsg, F, Ratio)
# bad ratio
self.assertRaises(TypeError, F, Ratio(7))
self.assertRaises(ValueError, F, Ratio((7,)))
self.assertRaises(ValueError, F, Ratio((7, 3, 1)))
# only single-argument form
self.assertRaises(TypeError, F, Ratio((3, 7)), 11)
self.assertRaises(TypeError, F, 2, Ratio((-10, 9)))
# as_integer_ratio not defined in a class
class A:
pass
a = A()
a.as_integer_ratio = lambda: (9, 5)
self.assertEqual((9, 5), _components(F(a)))
# as_integer_ratio defined in a metaclass
class M(type):
def as_integer_ratio(self):
return (11, 9)
class B(metaclass=M):
pass
self.assertRaisesRegex(TypeError, errmsg, F, B)
self.assertRaisesRegex(TypeError, errmsg, F, B())
def testFromString(self):
self.assertEqual((5, 1), _components(F("5")))
self.assertEqual((3, 2), _components(F("3/2")))

View File

@ -0,0 +1,2 @@
Added support for converting any objects that have the
:meth:`!as_integer_ratio` method to a :class:`~fractions.Fraction`.