Skip to content

Commit c5b6a2e

Browse files
committed
Speed up the internal creation of result Fractions if the calculated numerator/denominator pair is already normalised.
Follows python/cpython#101780
1 parent 475b58b commit c5b6a2e

File tree

3 files changed

+64
-27
lines changed

3 files changed

+64
-27
lines changed

CHANGES.rst

+5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ ChangeLog
1313
* ``Fraction.limit_denominator()`` is faster, following
1414
https://github.com/python/cpython/pull/93730
1515

16+
* Internal creation of result Fractions is about 10% faster if the calculated
17+
numerator/denominator pair is already normalised, following
18+
https://github.com/python/cpython/pull/101780
19+
20+
1621
1.13 (2022-01-11)
1722
-----------------
1823

src/quicktions.pyx

+58-27
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,8 @@ cdef object _FLOAT_FORMAT_SPECIFICATION_MATCHER = re.compile(r"""
330330
$
331331
""", re.DOTALL | re.VERBOSE).match
332332

333+
cdef object NOINIT = object()
334+
333335

334336
cdef class Fraction:
335337
"""A Rational number.
@@ -367,9 +369,12 @@ cdef class Fraction:
367369
cdef _denominator
368370
cdef Py_hash_t _hash
369371

370-
def __cinit__(self, numerator=0, denominator=None, *, bint _normalize=True):
371-
cdef Fraction value
372+
def __cinit__(self, numerator=0, denominator=None):
372373
self._hash = -1
374+
if numerator is NOINIT:
375+
return # fast-path for external initialisation
376+
377+
cdef bint _normalize = True
373378
if denominator is None:
374379
if type(numerator) is int or type(numerator) is long:
375380
self._numerator = numerator
@@ -500,14 +505,19 @@ cdef class Fraction:
500505
raise OverflowError(f"Cannot convert {dec} to {cls.__name__}.")
501506
if dec.is_nan():
502507
raise ValueError(f"Cannot convert {dec} to {cls.__name__}.")
508+
509+
if _decimal_supports_integer_ratio:
510+
num, denom = dec.as_integer_ratio()
511+
return _fraction_from_coprime_ints(num, denom, cls)
512+
503513
sign, digits, exp = dec.as_tuple()
504514
digits = int(''.join(map(str, digits)))
505515
if sign:
506516
digits = -digits
507517
if exp >= 0:
508-
return cls(digits * pow10(exp))
518+
return _fraction_from_coprime_ints(digits * pow10(exp), None, cls)
509519
else:
510-
return cls(digits, pow10(-exp))
520+
return _fraction_from_coprime_ints(digits, pow10(-exp), cls)
511521

512522
def is_integer(self):
513523
"""Return True if the Fraction is an integer."""
@@ -574,9 +584,9 @@ cdef class Fraction:
574584
# the distance from p1/q1 to self is d/(q1*self._denominator). So we
575585
# need to compare 2*(q0+k*q1) with self._denominator/d.
576586
if 2*d*(q0+k*q1) <= self._denominator:
577-
return Fraction(p1, q1, _normalize=False)
587+
return _fraction_from_coprime_ints(p1, q1)
578588
else:
579-
return Fraction(p0+k*p1, q0+k*q1, _normalize=False)
589+
return _fraction_from_coprime_ints(p0+k*p1, q0+k*q1)
580590

581591
@property
582592
def numerator(self):
@@ -839,15 +849,15 @@ cdef class Fraction:
839849
"""+a: Coerces a subclass instance to Fraction"""
840850
if type(a) is Fraction:
841851
return a
842-
return Fraction(a._numerator, a._denominator, _normalize=False)
852+
return _fraction_from_coprime_ints(a._numerator, a._denominator)
843853

844854
def __neg__(a):
845855
"""-a"""
846-
return Fraction(-a._numerator, a._denominator, _normalize=False)
856+
return _fraction_from_coprime_ints(-a._numerator, a._denominator)
847857

848858
def __abs__(a):
849859
"""abs(a)"""
850-
return Fraction(abs(a._numerator), a._denominator, _normalize=False)
860+
return _fraction_from_coprime_ints(abs(a._numerator), a._denominator)
851861

852862
def __int__(a):
853863
"""int(a)"""
@@ -1113,20 +1123,39 @@ cdef class Fraction:
11131123
Rational.register(Fraction)
11141124

11151125

1126+
cdef _fraction_from_coprime_ints(numerator, denominator, cls=None):
1127+
"""Convert a pair of ints to a rational number, for internal use.
1128+
1129+
The ratio of integers should be in lowest terms and the denominator
1130+
should be positive.
1131+
"""
1132+
cdef Fraction obj
1133+
if cls is None or cls is Fraction:
1134+
obj = Fraction.__new__(Fraction, NOINIT, NOINIT)
1135+
else:
1136+
obj = super(Fraction, cls).__new__(cls)
1137+
obj._numerator = numerator
1138+
obj._denominator = denominator
1139+
return obj
1140+
1141+
11161142
cdef _pow(an, ad, bn, bd):
11171143
if bd == 1:
1144+
# power = bn
11181145
if bn >= 0:
1119-
return Fraction(an ** bn,
1120-
ad ** bn,
1121-
_normalize=False)
1122-
elif an >= 0:
1123-
return Fraction(ad ** -bn,
1124-
an ** -bn,
1125-
_normalize=False)
1146+
return _fraction_from_coprime_ints(
1147+
an ** bn,
1148+
ad ** bn)
1149+
elif an > 0:
1150+
return _fraction_from_coprime_ints(
1151+
ad ** -bn,
1152+
an ** -bn)
1153+
elif an == 0:
1154+
raise ZeroDivisionError(f'Fraction({ad ** -bn}, 0)')
11261155
else:
1127-
return Fraction((-ad) ** -bn,
1128-
(-an) ** -bn,
1129-
_normalize=False)
1156+
return _fraction_from_coprime_ints(
1157+
(-ad) ** -bn,
1158+
(-an) ** -bn)
11301159
else:
11311160
# A fractional power will generally produce an
11321161
# irrational number.
@@ -1210,26 +1239,26 @@ cdef _add(na, da, nb, db):
12101239
# return Fraction(na * db + nb * da, da * db)
12111240
g = _gcd(da, db)
12121241
if g == 1:
1213-
return Fraction(na * db + da * nb, da * db, _normalize=False)
1242+
return _fraction_from_coprime_ints(na * db + da * nb, da * db)
12141243
s = da // g
12151244
t = na * (db // g) + nb * s
12161245
g2 = _gcd(t, g)
12171246
if g2 == 1:
1218-
return Fraction(t, s * db, _normalize=False)
1219-
return Fraction(t // g2, s * (db // g2), _normalize=False)
1247+
return _fraction_from_coprime_ints(t, s * db)
1248+
return _fraction_from_coprime_ints(t // g2, s * (db // g2))
12201249

12211250
cdef _sub(na, da, nb, db):
12221251
"""a - b"""
12231252
# return Fraction(na * db - nb * da, da * db)
12241253
g = _gcd(da, db)
12251254
if g == 1:
1226-
return Fraction(na * db - da * nb, da * db, _normalize=False)
1255+
return _fraction_from_coprime_ints(na * db - da * nb, da * db)
12271256
s = da // g
12281257
t = na * (db // g) - nb * s
12291258
g2 = _gcd(t, g)
12301259
if g2 == 1:
1231-
return Fraction(t, s * db, _normalize=False)
1232-
return Fraction(t // g2, s * (db // g2), _normalize=False)
1260+
return _fraction_from_coprime_ints(t, s * db)
1261+
return _fraction_from_coprime_ints(t // g2, s * (db // g2))
12331262

12341263
cdef _mul(na, da, nb, db):
12351264
"""a * b"""
@@ -1242,12 +1271,14 @@ cdef _mul(na, da, nb, db):
12421271
if g2 > 1:
12431272
nb //= g2
12441273
da //= g2
1245-
return Fraction(na * nb, db * da, _normalize=False)
1274+
return _fraction_from_coprime_ints(na * nb, db * da)
12461275

12471276
cdef _div(na, da, nb, db):
12481277
"""a / b"""
12491278
# return Fraction(na * db, da * nb)
12501279
# Same as _mul(), with inversed b.
1280+
if nb == 0:
1281+
raise ZeroDivisionError(f'Fraction({db}, 0)')
12511282
g1 = _gcd(na, nb)
12521283
if g1 > 1:
12531284
na //= g1
@@ -1259,7 +1290,7 @@ cdef _div(na, da, nb, db):
12591290
n, d = na * db, nb * da
12601291
if d < 0:
12611292
n, d = -n, -d
1262-
return Fraction(n, d, _normalize=False)
1293+
return _fraction_from_coprime_ints(n, d)
12631294

12641295
cdef _floordiv(an, ad, bn, bd):
12651296
"""a // b -> int"""

src/test_fractions.py

+1
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,7 @@ def testArithmetic(self):
645645
self.assertEqual(F(5, 6), F(2, 3) * F(5, 4))
646646
self.assertEqual(F(1, 4), F(1, 10) / F(2, 5))
647647
self.assertEqual(F(-15, 8), F(3, 4) / F(-2, 5))
648+
self.assertRaises(ZeroDivisionError, operator.truediv, F(1), F(0))
648649
self.assertTypedEquals(2, F(9, 10) // F(2, 5))
649650
self.assertTypedEquals(10**23, F(10**23, 1) // F(1))
650651
self.assertEqual(F(5, 6), F(7, 3) % F(3, 2))

0 commit comments

Comments
 (0)