Skip to content

Commit c8d2630

Browse files
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.
1 parent eaf094c commit c8d2630

File tree

5 files changed

+66
-13
lines changed

5 files changed

+66
-13
lines changed

Doc/library/fractions.rst

+18-9
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,30 @@ The :mod:`fractions` module provides support for rational number arithmetic.
1717
A Fraction instance can be constructed from a pair of integers, from
1818
another rational number, or from a string.
1919

20+
.. index:: single: as_integer_ratio()
21+
2022
.. class:: Fraction(numerator=0, denominator=1)
21-
Fraction(other_fraction)
22-
Fraction(float)
23-
Fraction(decimal)
23+
Fraction(number)
2424
Fraction(string)
2525

2626
The first version requires that *numerator* and *denominator* are instances
2727
of :class:`numbers.Rational` and returns a new :class:`Fraction` instance
2828
with value ``numerator/denominator``. If *denominator* is ``0``, it
29-
raises a :exc:`ZeroDivisionError`. The second version requires that
30-
*other_fraction* is an instance of :class:`numbers.Rational` and returns a
31-
:class:`Fraction` instance with the same value. The next two versions accept
32-
either a :class:`float` or a :class:`decimal.Decimal` instance, and return a
33-
:class:`Fraction` instance with exactly the same value. Note that due to the
29+
raises a :exc:`ZeroDivisionError`.
30+
31+
The second version requires that *number* is an instance of
32+
:class:`numbers.Rational` or has the :meth:`!as_integer_ratio` method
33+
(this includes :class:`float` and :class:`decimal.Decimal`).
34+
It returns a :class:`Fraction` instance with exactly the same value.
35+
Assumed, that the :meth:`!as_integer_ratio` method returns a pair
36+
of coprime integers and last one is positive.
37+
Note that due to the
3438
usual issues with binary floating-point (see :ref:`tut-fp-issues`), the
3539
argument to ``Fraction(1.1)`` is not exactly equal to 11/10, and so
3640
``Fraction(1.1)`` does *not* return ``Fraction(11, 10)`` as one might expect.
3741
(But see the documentation for the :meth:`limit_denominator` method below.)
38-
The last version of the constructor expects a string or unicode instance.
42+
43+
The last version of the constructor expects a string.
3944
The usual form for this instance is::
4045

4146
[sign] numerator ['/' denominator]
@@ -110,6 +115,10 @@ another rational number, or from a string.
110115
Formatting of :class:`Fraction` instances without a presentation type
111116
now supports fill, alignment, sign handling, minimum width and grouping.
112117

118+
.. versionchanged:: 3.14
119+
The :class:`Fraction` constructor now accepts any objects with the
120+
:meth:`!as_integer_ratio` method.
121+
113122
.. attribute:: numerator
114123

115124
Numerator of the Fraction in lowest term.

Doc/whatsnew/3.14.rst

+7
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ ast
100100

101101
(Contributed by Bénédikt Tran in :gh:`121141`.)
102102

103+
fractions
104+
---------
105+
106+
Added support for converting any objects that have the
107+
:meth:`!as_integer_ratio` method to a :class:`~fractions.Fraction`.
108+
(Contributed by Serhiy Storchaka in :gh:`82017`.)
109+
103110
os
104111
--
105112

Lib/fractions.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
"""Fraction, infinite-precision, rational numbers."""
55

6-
from decimal import Decimal
76
import functools
87
import math
98
import numbers
@@ -244,7 +243,9 @@ def __new__(cls, numerator=0, denominator=None):
244243
self._denominator = numerator.denominator
245244
return self
246245

247-
elif isinstance(numerator, (float, Decimal)):
246+
elif (isinstance(numerator, float) or
247+
(not isinstance(numerator, type) and
248+
hasattr(numerator, 'as_integer_ratio'))):
248249
# Exact conversion
249250
self._numerator, self._denominator = numerator.as_integer_ratio()
250251
return self
@@ -278,8 +279,7 @@ def __new__(cls, numerator=0, denominator=None):
278279
numerator = -numerator
279280

280281
else:
281-
raise TypeError("argument should be a string "
282-
"or a Rational instance")
282+
raise TypeError("argument should be a string or a number")
283283

284284
elif type(numerator) is int is type(denominator):
285285
pass # *very* normal case

Lib/test/test_fractions.py

+35
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,41 @@ def testInitFromDecimal(self):
354354
self.assertRaises(OverflowError, F, Decimal('inf'))
355355
self.assertRaises(OverflowError, F, Decimal('-inf'))
356356

357+
def testInitFromIntegerRatio(self):
358+
class Ratio:
359+
def __init__(self, ratio):
360+
self._ratio = ratio
361+
def as_integer_ratio(self):
362+
return self._ratio
363+
364+
self.assertEqual((7, 3), _components(F(Ratio((7, 3)))))
365+
errmsg = "argument should be a string or a number"
366+
# the type also has an "as_integer_ratio" attribute.
367+
self.assertRaisesRegex(TypeError, errmsg, F, Ratio)
368+
# bad ratio
369+
self.assertRaises(TypeError, F, Ratio(7))
370+
self.assertRaises(ValueError, F, Ratio((7,)))
371+
self.assertRaises(ValueError, F, Ratio((7, 3, 1)))
372+
# only single-argument form
373+
self.assertRaises(TypeError, F, Ratio((3, 7)), 11)
374+
self.assertRaises(TypeError, F, 2, Ratio((-10, 9)))
375+
376+
# as_integer_ratio not defined in a class
377+
class A:
378+
pass
379+
a = A()
380+
a.as_integer_ratio = lambda: (9, 5)
381+
self.assertEqual((9, 5), _components(F(a)))
382+
383+
# as_integer_ratio defined in a metaclass
384+
class M(type):
385+
def as_integer_ratio(self):
386+
return (11, 9)
387+
class B(metaclass=M):
388+
pass
389+
self.assertRaisesRegex(TypeError, errmsg, F, B)
390+
self.assertRaisesRegex(TypeError, errmsg, F, B())
391+
357392
def testFromString(self):
358393
self.assertEqual((5, 1), _components(F("5")))
359394
self.assertEqual((3, 2), _components(F("3/2")))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added support for converting any objects that have the
2+
:meth:`!as_integer_ratio` method to a :class:`~fractions.Fraction`.

0 commit comments

Comments
 (0)