Index: Doc/library/fractions.rst =================================================================== --- Doc/library/fractions.rst (revision 79614) +++ Doc/library/fractions.rst (working copy) @@ -17,17 +17,24 @@ .. 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 an :class:`Fraction` instance with the same value. The - last version of the constructor expects a string or unicode - instance. The usual form for this instance is:: + 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 an + :class:`Fraction` instance with the same value. The next two versions accept + either a :class:`float` or a :class:`decimal.Decimal` instance, and return an + :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 + ``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:: [sign] numerator ['/' denominator] @@ -57,6 +64,13 @@ Fraction(-1, 8) >>> Fraction('7e-6') Fraction(7, 1000000) + >>> Fraction(2.25) + Fraction(9, 4) + >>> Fraction(1.1) + Fraction(2476979795053773, 2251799813685248) + >>> from decimal import Decimal + >>> Fraction(Decimal('1.1')) + Fraction(11, 10) The :class:`Fraction` class inherits from the abstract base class @@ -65,20 +79,30 @@ and should be treated as immutable. In addition, :class:`Fraction` has the following methods: + .. versionchanged:: 2.7 + The :class:`Fraction` constructor now accepts :class:`float` and + :class:`decimal.Decimal` instances. + .. method:: from_float(flt) This class method constructs a :class:`Fraction` representing the exact value of *flt*, which must be a :class:`float`. Beware that ``Fraction.from_float(0.3)`` is not the same value as ``Fraction(3, 10)`` + .. note:: From Python 2.7 onwards, you can also construct a + :class:`Fraction` instance directly from a :class:`float`. + .. method:: from_decimal(dec) This class method constructs a :class:`Fraction` representing the exact value of *dec*, which must be a :class:`decimal.Decimal`. + .. note:: From Python 2.7 onwards, you can also construct a + :class:`Fraction` instance directly from a :class:`decimal.Decimal`. + .. method:: limit_denominator(max_denominator=1000000) Finds and returns the closest :class:`Fraction` to ``self`` that has @@ -92,10 +116,12 @@ or for recovering a rational number that's represented as a float: >>> from math import pi, cos - >>> Fraction.from_float(cos(pi/3)) + >>> Fraction(cos(pi/3)) Fraction(4503599627370497, 9007199254740992) - >>> Fraction.from_float(cos(pi/3)).limit_denominator() + >>> Fraction(cos(pi/3)).limit_denominator() Fraction(1, 2) + >>> Fraction(1.1).limit_denominator() + Fraction(11, 10) .. function:: gcd(a, b) Index: Lib/fractions.py =================================================================== --- Lib/fractions.py (revision 79614) +++ Lib/fractions.py (working copy) @@ -4,6 +4,7 @@ """Rational, infinite-precision, real numbers.""" from __future__ import division +from decimal import Decimal import math import numbers import operator @@ -43,14 +44,22 @@ class Fraction(Rational): """This class implements rational numbers. - Fraction(8, 6) will produce a rational number equivalent to - 4/3. Both arguments must be Integral. The numerator defaults to 0 - and the denominator defaults to 1 so that Fraction(3) == 3 and - Fraction() == 0. + 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. - Fractions can also be constructed from strings of the form - '[-+]?[0-9]+((/|.)[0-9]+)?', optionally surrounded by spaces. + Fractions can also be constructed from: + - 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') @@ -59,9 +68,33 @@ def __new__(cls, numerator=0, denominator=None): """Constructs a Fraction. - Takes a string like '3/2' or '1.5', another Fraction, or a - numerator/denominator pair. + Takes a string like '3/2' or '1.5', another Rational instance, a + numerator/denominator pair, or a float. + Examples + -------- + + >>> Fraction(10, -8) + Fraction(-5, 4) + >>> Fraction(Fraction(1, 7), 5) + Fraction(1, 35) + >>> Fraction(Fraction(1, 7), Fraction(2, 3)) + Fraction(3, 14) + >>> Fraction('314') + Fraction(314, 1) + >>> Fraction('-35/4') + Fraction(-35, 4) + >>> Fraction('3.1415') # conversion from numeric string + Fraction(6283, 2000) + >>> Fraction('-47e-2') # string may include a decimal exponent + Fraction(-47, 100) + >>> Fraction(1.47) # direct construction from float (exact conversion) + Fraction(6620291452234629, 4503599627370496) + >>> Fraction(2.25) + Fraction(9, 4) + >>> Fraction(Decimal('1.47')) + Fraction(147, 100) + """ self = super(Fraction, cls).__new__(cls) @@ -71,6 +104,19 @@ self._denominator = numerator.denominator return self + elif isinstance(numerator, float): + # Exact conversion from float + value = Fraction.from_float(numerator) + self._numerator = value._numerator + self._denominator = value._denominator + return self + + elif isinstance(numerator, Decimal): + value = Fraction.from_decimal(numerator) + self._numerator = value._numerator + self._denominator = value._denominator + return self + elif isinstance(numerator, basestring): # Handle construction from strings. m = _RATIONAL_FORMAT.match(numerator) Index: Lib/test/test_fractions.py =================================================================== --- Lib/test/test_fractions.py (revision 79614) +++ Lib/test/test_fractions.py (working copy) @@ -12,6 +12,11 @@ F = fractions.Fraction gcd = fractions.gcd +# decorator for skipping tests on non-IEEE 754 platforms +requires_IEEE_754 = unittest.skipUnless( + float.__getformat__("double").startswith("IEEE"), + "test requires IEEE 754 doubles") + class DummyFloat(object): """Dummy float class for testing comparisons with Fractions""" @@ -137,14 +142,34 @@ self.assertRaisesMessage(ZeroDivisionError, "Fraction(12, 0)", F, 12, 0) - self.assertRaises(TypeError, F, 1.5) self.assertRaises(TypeError, F, 1.5 + 3j) self.assertRaises(TypeError, F, "3/2", 3) self.assertRaises(TypeError, F, 3, 0j) self.assertRaises(TypeError, F, 3, 1j) + @requires_IEEE_754 + def testInitFromFloat(self): + self.assertEquals((5, 2), _components(F(2.5))) + self.assertEquals((0, 1), _components(F(-0.0))) + self.assertEquals((3602879701896397, 36028797018963968), + _components(F(0.1))) + self.assertRaises(TypeError, F, float('nan')) + self.assertRaises(TypeError, F, float('inf')) + self.assertRaises(TypeError, F, float('-inf')) + def testInitFromDecimal(self): + self.assertEquals((11, 10), + _components(F(Decimal('1.1')))) + self.assertEquals((7, 200), + _components(F(Decimal('3.5e-2')))) + self.assertEquals((0, 1), + _components(F(Decimal('.000e20')))) + self.assertRaises(TypeError, F, Decimal('nan')) + self.assertRaises(TypeError, F, Decimal('snan')) + self.assertRaises(TypeError, F, Decimal('inf')) + self.assertRaises(TypeError, F, Decimal('-inf')) + def testFromString(self): self.assertEquals((5, 1), _components(F("5"))) self.assertEquals((3, 2), _components(F("3/2"))) @@ -244,6 +269,7 @@ TypeError, "Cannot convert nan to Fraction.", F.from_float, nan) + def testFromDecimal(self): self.assertRaises(TypeError, F.from_decimal, 3+4j) self.assertEquals(F(10, 1), F.from_decimal(10))