mirror of https://github.com/python/cpython
gh-101773: Optimize creation of Fractions in private methods (#101780)
This PR adds a private `Fraction._from_coprime_ints` classmethod for internal creations of `Fraction` objects, replacing the use of `_normalize=False` in the existing constructor. This speeds up creation of `Fraction` objects arising from calculations. The `_normalize` argument to the `Fraction` constructor has been removed. Co-authored-by: Pieter Eendebak <pieter.eendebak@gmail.com> Co-authored-by: Mark Dickinson <dickinsm@gmail.com>
This commit is contained in:
parent
bb0cf8fd60
commit
4f3786b761
|
@ -183,7 +183,7 @@ class Fraction(numbers.Rational):
|
||||||
__slots__ = ('_numerator', '_denominator')
|
__slots__ = ('_numerator', '_denominator')
|
||||||
|
|
||||||
# We're immutable, so use __new__ not __init__
|
# We're immutable, so use __new__ not __init__
|
||||||
def __new__(cls, numerator=0, denominator=None, *, _normalize=True):
|
def __new__(cls, numerator=0, denominator=None):
|
||||||
"""Constructs a Rational.
|
"""Constructs a Rational.
|
||||||
|
|
||||||
Takes a string like '3/2' or '1.5', another Rational instance, a
|
Takes a string like '3/2' or '1.5', another Rational instance, a
|
||||||
|
@ -279,12 +279,11 @@ class Fraction(numbers.Rational):
|
||||||
|
|
||||||
if denominator == 0:
|
if denominator == 0:
|
||||||
raise ZeroDivisionError('Fraction(%s, 0)' % numerator)
|
raise ZeroDivisionError('Fraction(%s, 0)' % numerator)
|
||||||
if _normalize:
|
g = math.gcd(numerator, denominator)
|
||||||
g = math.gcd(numerator, denominator)
|
if denominator < 0:
|
||||||
if denominator < 0:
|
g = -g
|
||||||
g = -g
|
numerator //= g
|
||||||
numerator //= g
|
denominator //= g
|
||||||
denominator //= g
|
|
||||||
self._numerator = numerator
|
self._numerator = numerator
|
||||||
self._denominator = denominator
|
self._denominator = denominator
|
||||||
return self
|
return self
|
||||||
|
@ -301,7 +300,7 @@ class Fraction(numbers.Rational):
|
||||||
elif not isinstance(f, float):
|
elif not isinstance(f, float):
|
||||||
raise TypeError("%s.from_float() only takes floats, not %r (%s)" %
|
raise TypeError("%s.from_float() only takes floats, not %r (%s)" %
|
||||||
(cls.__name__, f, type(f).__name__))
|
(cls.__name__, f, type(f).__name__))
|
||||||
return cls(*f.as_integer_ratio())
|
return cls._from_coprime_ints(*f.as_integer_ratio())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_decimal(cls, dec):
|
def from_decimal(cls, dec):
|
||||||
|
@ -313,7 +312,19 @@ class Fraction(numbers.Rational):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"%s.from_decimal() only takes Decimals, not %r (%s)" %
|
"%s.from_decimal() only takes Decimals, not %r (%s)" %
|
||||||
(cls.__name__, dec, type(dec).__name__))
|
(cls.__name__, dec, type(dec).__name__))
|
||||||
return cls(*dec.as_integer_ratio())
|
return cls._from_coprime_ints(*dec.as_integer_ratio())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_coprime_ints(cls, numerator, denominator, /):
|
||||||
|
"""Convert a pair of ints to a rational number, for internal use.
|
||||||
|
|
||||||
|
The ratio of integers should be in lowest terms and the denominator
|
||||||
|
should be positive.
|
||||||
|
"""
|
||||||
|
obj = super(Fraction, cls).__new__(cls)
|
||||||
|
obj._numerator = numerator
|
||||||
|
obj._denominator = denominator
|
||||||
|
return obj
|
||||||
|
|
||||||
def is_integer(self):
|
def is_integer(self):
|
||||||
"""Return True if the Fraction is an integer."""
|
"""Return True if the Fraction is an integer."""
|
||||||
|
@ -380,9 +391,9 @@ class Fraction(numbers.Rational):
|
||||||
# the distance from p1/q1 to self is d/(q1*self._denominator). So we
|
# the distance from p1/q1 to self is d/(q1*self._denominator). So we
|
||||||
# need to compare 2*(q0+k*q1) with self._denominator/d.
|
# need to compare 2*(q0+k*q1) with self._denominator/d.
|
||||||
if 2*d*(q0+k*q1) <= self._denominator:
|
if 2*d*(q0+k*q1) <= self._denominator:
|
||||||
return Fraction(p1, q1, _normalize=False)
|
return Fraction._from_coprime_ints(p1, q1)
|
||||||
else:
|
else:
|
||||||
return Fraction(p0+k*p1, q0+k*q1, _normalize=False)
|
return Fraction._from_coprime_ints(p0+k*p1, q0+k*q1)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def numerator(a):
|
def numerator(a):
|
||||||
|
@ -703,13 +714,13 @@ class Fraction(numbers.Rational):
|
||||||
nb, db = b._numerator, b._denominator
|
nb, db = b._numerator, b._denominator
|
||||||
g = math.gcd(da, db)
|
g = math.gcd(da, db)
|
||||||
if g == 1:
|
if g == 1:
|
||||||
return Fraction(na * db + da * nb, da * db, _normalize=False)
|
return Fraction._from_coprime_ints(na * db + da * nb, da * db)
|
||||||
s = da // g
|
s = da // g
|
||||||
t = na * (db // g) + nb * s
|
t = na * (db // g) + nb * s
|
||||||
g2 = math.gcd(t, g)
|
g2 = math.gcd(t, g)
|
||||||
if g2 == 1:
|
if g2 == 1:
|
||||||
return Fraction(t, s * db, _normalize=False)
|
return Fraction._from_coprime_ints(t, s * db)
|
||||||
return Fraction(t // g2, s * (db // g2), _normalize=False)
|
return Fraction._from_coprime_ints(t // g2, s * (db // g2))
|
||||||
|
|
||||||
__add__, __radd__ = _operator_fallbacks(_add, operator.add)
|
__add__, __radd__ = _operator_fallbacks(_add, operator.add)
|
||||||
|
|
||||||
|
@ -719,13 +730,13 @@ class Fraction(numbers.Rational):
|
||||||
nb, db = b._numerator, b._denominator
|
nb, db = b._numerator, b._denominator
|
||||||
g = math.gcd(da, db)
|
g = math.gcd(da, db)
|
||||||
if g == 1:
|
if g == 1:
|
||||||
return Fraction(na * db - da * nb, da * db, _normalize=False)
|
return Fraction._from_coprime_ints(na * db - da * nb, da * db)
|
||||||
s = da // g
|
s = da // g
|
||||||
t = na * (db // g) - nb * s
|
t = na * (db // g) - nb * s
|
||||||
g2 = math.gcd(t, g)
|
g2 = math.gcd(t, g)
|
||||||
if g2 == 1:
|
if g2 == 1:
|
||||||
return Fraction(t, s * db, _normalize=False)
|
return Fraction._from_coprime_ints(t, s * db)
|
||||||
return Fraction(t // g2, s * (db // g2), _normalize=False)
|
return Fraction._from_coprime_ints(t // g2, s * (db // g2))
|
||||||
|
|
||||||
__sub__, __rsub__ = _operator_fallbacks(_sub, operator.sub)
|
__sub__, __rsub__ = _operator_fallbacks(_sub, operator.sub)
|
||||||
|
|
||||||
|
@ -741,15 +752,17 @@ class Fraction(numbers.Rational):
|
||||||
if g2 > 1:
|
if g2 > 1:
|
||||||
nb //= g2
|
nb //= g2
|
||||||
da //= g2
|
da //= g2
|
||||||
return Fraction(na * nb, db * da, _normalize=False)
|
return Fraction._from_coprime_ints(na * nb, db * da)
|
||||||
|
|
||||||
__mul__, __rmul__ = _operator_fallbacks(_mul, operator.mul)
|
__mul__, __rmul__ = _operator_fallbacks(_mul, operator.mul)
|
||||||
|
|
||||||
def _div(a, b):
|
def _div(a, b):
|
||||||
"""a / b"""
|
"""a / b"""
|
||||||
# Same as _mul(), with inversed b.
|
# Same as _mul(), with inversed b.
|
||||||
na, da = a._numerator, a._denominator
|
|
||||||
nb, db = b._numerator, b._denominator
|
nb, db = b._numerator, b._denominator
|
||||||
|
if nb == 0:
|
||||||
|
raise ZeroDivisionError('Fraction(%s, 0)' % db)
|
||||||
|
na, da = a._numerator, a._denominator
|
||||||
g1 = math.gcd(na, nb)
|
g1 = math.gcd(na, nb)
|
||||||
if g1 > 1:
|
if g1 > 1:
|
||||||
na //= g1
|
na //= g1
|
||||||
|
@ -761,7 +774,7 @@ class Fraction(numbers.Rational):
|
||||||
n, d = na * db, nb * da
|
n, d = na * db, nb * da
|
||||||
if d < 0:
|
if d < 0:
|
||||||
n, d = -n, -d
|
n, d = -n, -d
|
||||||
return Fraction(n, d, _normalize=False)
|
return Fraction._from_coprime_ints(n, d)
|
||||||
|
|
||||||
__truediv__, __rtruediv__ = _operator_fallbacks(_div, operator.truediv)
|
__truediv__, __rtruediv__ = _operator_fallbacks(_div, operator.truediv)
|
||||||
|
|
||||||
|
@ -798,17 +811,17 @@ class Fraction(numbers.Rational):
|
||||||
if b.denominator == 1:
|
if b.denominator == 1:
|
||||||
power = b.numerator
|
power = b.numerator
|
||||||
if power >= 0:
|
if power >= 0:
|
||||||
return Fraction(a._numerator ** power,
|
return Fraction._from_coprime_ints(a._numerator ** power,
|
||||||
a._denominator ** power,
|
a._denominator ** power)
|
||||||
_normalize=False)
|
elif a._numerator > 0:
|
||||||
elif a._numerator >= 0:
|
return Fraction._from_coprime_ints(a._denominator ** -power,
|
||||||
return Fraction(a._denominator ** -power,
|
a._numerator ** -power)
|
||||||
a._numerator ** -power,
|
elif a._numerator == 0:
|
||||||
_normalize=False)
|
raise ZeroDivisionError('Fraction(%s, 0)' %
|
||||||
|
a._denominator ** -power)
|
||||||
else:
|
else:
|
||||||
return Fraction((-a._denominator) ** -power,
|
return Fraction._from_coprime_ints((-a._denominator) ** -power,
|
||||||
(-a._numerator) ** -power,
|
(-a._numerator) ** -power)
|
||||||
_normalize=False)
|
|
||||||
else:
|
else:
|
||||||
# A fractional power will generally produce an
|
# A fractional power will generally produce an
|
||||||
# irrational number.
|
# irrational number.
|
||||||
|
@ -832,15 +845,15 @@ class Fraction(numbers.Rational):
|
||||||
|
|
||||||
def __pos__(a):
|
def __pos__(a):
|
||||||
"""+a: Coerces a subclass instance to Fraction"""
|
"""+a: Coerces a subclass instance to Fraction"""
|
||||||
return Fraction(a._numerator, a._denominator, _normalize=False)
|
return Fraction._from_coprime_ints(a._numerator, a._denominator)
|
||||||
|
|
||||||
def __neg__(a):
|
def __neg__(a):
|
||||||
"""-a"""
|
"""-a"""
|
||||||
return Fraction(-a._numerator, a._denominator, _normalize=False)
|
return Fraction._from_coprime_ints(-a._numerator, a._denominator)
|
||||||
|
|
||||||
def __abs__(a):
|
def __abs__(a):
|
||||||
"""abs(a)"""
|
"""abs(a)"""
|
||||||
return Fraction(abs(a._numerator), a._denominator, _normalize=False)
|
return Fraction._from_coprime_ints(abs(a._numerator), a._denominator)
|
||||||
|
|
||||||
def __int__(a, _index=operator.index):
|
def __int__(a, _index=operator.index):
|
||||||
"""int(a)"""
|
"""int(a)"""
|
||||||
|
|
|
@ -488,6 +488,7 @@ class FractionTest(unittest.TestCase):
|
||||||
self.assertEqual(F(5, 6), F(2, 3) * F(5, 4))
|
self.assertEqual(F(5, 6), F(2, 3) * F(5, 4))
|
||||||
self.assertEqual(F(1, 4), F(1, 10) / F(2, 5))
|
self.assertEqual(F(1, 4), F(1, 10) / F(2, 5))
|
||||||
self.assertEqual(F(-15, 8), F(3, 4) / F(-2, 5))
|
self.assertEqual(F(-15, 8), F(3, 4) / F(-2, 5))
|
||||||
|
self.assertRaises(ZeroDivisionError, operator.truediv, F(1), F(0))
|
||||||
self.assertTypedEquals(2, F(9, 10) // F(2, 5))
|
self.assertTypedEquals(2, F(9, 10) // F(2, 5))
|
||||||
self.assertTypedEquals(10**23, F(10**23, 1) // F(1))
|
self.assertTypedEquals(10**23, F(10**23, 1) // F(1))
|
||||||
self.assertEqual(F(5, 6), F(7, 3) % F(3, 2))
|
self.assertEqual(F(5, 6), F(7, 3) % F(3, 2))
|
||||||
|
|
|
@ -145,7 +145,7 @@ class HashTest(unittest.TestCase):
|
||||||
# The numbers ABC doesn't enforce that the "true" division
|
# The numbers ABC doesn't enforce that the "true" division
|
||||||
# of integers produces a float. This tests that the
|
# of integers produces a float. This tests that the
|
||||||
# Rational.__float__() method has required type conversions.
|
# Rational.__float__() method has required type conversions.
|
||||||
x = F(DummyIntegral(1), DummyIntegral(2), _normalize=False)
|
x = F._from_coprime_ints(DummyIntegral(1), DummyIntegral(2))
|
||||||
self.assertRaises(TypeError, lambda: x.numerator/x.denominator)
|
self.assertRaises(TypeError, lambda: x.numerator/x.denominator)
|
||||||
self.assertEqual(float(x), 0.5)
|
self.assertEqual(float(x), 0.5)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Optimize :class:`fractions.Fraction` for small components. The private argument
|
||||||
|
``_normalize`` of the :class:`fractions.Fraction` constructor has been removed.
|
Loading…
Reference in New Issue