FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

gh-121797: Add class method Fraction.from_number() by serhiy-storchaka · Pull Request #121800 · python/cpython · GitHub

/ cpython Public
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .py  (2) .rst  (3) All 2 file types selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
10 changes: 10 additions & 0 deletions Doc/library/fractions.rst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,16 @@ another rational number, or from a string.
instance.


.. classmethod:: from_number(number)

Alternative constructor which only accepts instances of
:class:`numbers.Integral`, :class:`numbers.Rational`,
:class:`float` or :class:`decimal.Decimal`, and objects with
the :meth:`!as_integer_ratio` method, but not strings.

.. versionadded:: 3.14


.. method:: limit_denominator(max_denominator=1000000)

Finds and returns the closest :class:`Fraction` to ``self`` that has
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.14.rst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,10 @@ fractions
:meth:`!as_integer_ratio` method to a :class:`~fractions.Fraction`.
(Contributed by Serhiy Storchaka in :gh:`82017`.)

* Add alternative :class:`~fractions.Fraction` constructor
:meth:`Fraction.from_number() <fractions.Fraction.from_number>`.
(Contributed by Serhiy Storchaka in :gh:`121797`.)


functools
---------
Expand Down
25 changes: 24 additions & 1 deletion Lib/fractions.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,8 @@ def __new__(cls, numerator=0, denominator=None):
numerator = -numerator

else:
raise TypeError("argument should be a string or a number")
raise TypeError("argument should be a string or a Rational "
"instance or have the as_integer_ratio() method")

elif type(numerator) is int is type(denominator):
pass # *very* normal case
Expand All @@ -305,6 +306,28 @@ def __new__(cls, numerator=0, denominator=None):
self._denominator = denominator
return self

@classmethod
def from_number(cls, number):

skirpichev Oct 13, 2024
edited
Loading

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Hmm, you can reuse this in the RationalFraction constructor, isn't? First three if/elif's share exactly same logic.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Do you mean the main Fraction constructor?

There is a different exception if the argument type is not supported. There is also a matter of performance.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I meant some helper:

@classmethod
def _from_number(cls, number):
    if type(number) is int:
        return cls._from_coprime_ints(number, 1)
    elif isinstance(number, numbers.Rational):
        return cls._from_coprime_ints(number.numerator, number.denominator)
    elif (isinstance(number, float) or
          (not isinstance(number, type) and
           hasattr(number, 'as_integer_ratio'))):
        return cls._from_coprime_ints(*number.as_integer_ratio())

Then you can reuse one in the constructor and in the from_number(), e.g.:

        if denominator is None:
            self = cls._from_number(numerator)
            if self is not None:
                return self

            elif isinstance(numerator, str):            
                ...

There is also a matter of performance.

I don't expect too much from extra class method call.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Extra function call and extra check.

This will not save much code. I'll leave this on future if we touch this code again.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Well, it's up to you. PR has Mark approval, so it's ready to go.

"""Converts a finite real number to a rational number, exactly.

Beware that Fraction.from_number(0.3) != Fraction(3, 10).

"""
if type(number) is int:
return cls._from_coprime_ints(number, 1)

elif isinstance(number, numbers.Rational):
return cls._from_coprime_ints(number.numerator, number.denominator)

elif (isinstance(number, float) or
(not isinstance(number, type) and
hasattr(number, 'as_integer_ratio'))):
return cls._from_coprime_ints(*number.as_integer_ratio())

else:
raise TypeError("argument should be a Rational instance or "
"have the as_integer_ratio() method")

@classmethod
def from_float(cls, f):
"""Converts a finite float to a rational number, exactly.
Expand Down
49 changes: 42 additions & 7 deletions Lib/test/test_fractions.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,13 @@ def __repr__(self):
class RectComplex(Rect, complex):
pass

class Ratio:
def __init__(self, ratio):
self._ratio = ratio
def as_integer_ratio(self):
return self._ratio


class FractionTest(unittest.TestCase):

def assertTypedEquals(self, expected, actual):
Expand Down Expand Up @@ -355,14 +362,9 @@ def testInitFromDecimal(self):
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"
errmsg = (r"argument should be a string or a Rational instance or "
r"have the as_integer_ratio\(\) method")
# the type also has an "as_integer_ratio" attribute.
self.assertRaisesRegex(TypeError, errmsg, F, Ratio)
# bad ratio
Expand All @@ -388,6 +390,8 @@ class B(metaclass=M):
pass
self.assertRaisesRegex(TypeError, errmsg, F, B)
self.assertRaisesRegex(TypeError, errmsg, F, B())
self.assertRaises(TypeError, F.from_number, B)
self.assertRaises(TypeError, F.from_number, B())

def testFromString(self):
self.assertEqual((5, 1), _components(F("5")))
Expand Down Expand Up @@ -594,6 +598,37 @@ def testFromDecimal(self):
ValueError, "cannot convert NaN to integer ratio",
F.from_decimal, Decimal("snan"))

def testFromNumber(self, cls=F):
def check(arg, numerator, denominator):
f = cls.from_number(arg)
self.assertIs(type(f), cls)
self.assertEqual(f.numerator, numerator)
self.assertEqual(f.denominator, denominator)

check(10, 10, 1)
check(2.5, 5, 2)
check(Decimal('2.5'), 5, 2)
check(F(22, 7), 22, 7)
check(DummyFraction(22, 7), 22, 7)
check(Rat(22, 7), 22, 7)
check(Ratio((22, 7)), 22, 7)
self.assertRaises(TypeError, cls.from_number, 3+4j)
self.assertRaises(TypeError, cls.from_number, '5/2')
self.assertRaises(TypeError, cls.from_number, [])
self.assertRaises(OverflowError, cls.from_number, float('inf'))
self.assertRaises(OverflowError, cls.from_number, Decimal('inf'))

# as_integer_ratio not defined in a class
class A:
pass
a = A()
a.as_integer_ratio = lambda: (9, 5)
check(a, 9, 5)

def testFromNumber_subclass(self):
self.testFromNumber(DummyFraction)


def test_is_integer(self):
self.assertTrue(F(1, 1).is_integer())
self.assertTrue(F(-1, 1).is_integer())
Expand Down
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add alternative :class:`~fractions.Fraction` constructor
:meth:`Fraction.from_number() <fractions.Fraction.from_number>`.

Back | FazBrowse Home | New Git URL