Description
Feature or enhancement
Proposal:
Currently, most methods of the Fraction class subclasses return an instance of the Fraction class. That happens for arithmetic methods as well:
>>> from fractions import Fraction
>>> class MyFraction(Fraction):
... pass
... a = MyFraction(1, 2)
... b = MyFraction(2, 3)
...
>>> a+b
Fraction(7, 6)
I would guess, it was intentional.
On another hand, this makes subclassing of the Fraction - less useful. For example, what if we want to use something other than builtin int's for components of a fraction? Currently, it's possible, but... not quite:
>>> from gmpy2 import mpz
>>> from fractions import Fraction
>>> class mpq(Fraction):
... def __new__(cls, numerator=0, denominator=None):
... self = super(mpq, cls).__new__(cls, numerator, denominator)
... self._numerator = mpz(numerator)
... self._denominator = mpz(denominator)
... return self
...
>>> a, b = mpq(1, 2), mpq(3, 4)
>>> c = a + b
>>> c._numerator # subclass instances use fast integer arithmetic
mpz(5)
>>> c._denominator
mpz(4)
>>> c # but it's still an instance of the Fraction!
Fraction(5, 4)
Attached patch fixes this.
To better support such subclasses, I think we could also add Fraction.gcd class attribute to override the math.gcd().
Of course, this is a compatibility break (patch breaks our CI tests). Though, looking on the GitHub search for subclasses of Fraction, I don't think this will really break some people code.
A quick patch
diff --git a/Lib/fractions.py b/Lib/fractions.py
index cb05ae7c20..d81be2f90e 100644
--- a/Lib/fractions.py
+++ b/Lib/fractions.py
@@ -411,8 +411,9 @@ def limit_denominator(self, max_denominator=1000000):
if max_denominator < 1:
raise ValueError("max_denominator should be at least 1")
+ cls = self.__class__
if self._denominator <= max_denominator:
- return Fraction(self)
+ return cls(self)
p0, q0, p1, q1 = 0, 1, 1, 0
n, d = self._numerator, self._denominator
@@ -430,9 +431,9 @@ def limit_denominator(self, max_denominator=1000000):
# 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.
if 2*d*(q0+k*q1) <= self._denominator:
- return Fraction._from_coprime_ints(p1, q1)
+ return cls._from_coprime_ints(p1, q1)
else:
- return Fraction._from_coprime_ints(p0+k*p1, q0+k*q1)
+ return cls._from_coprime_ints(p0+k*p1, q0+k*q1)
@property
def numerator(a):
@@ -782,38 +783,41 @@ def reverse(b, a):
def _add(a, b):
"""a + b"""
+ cls = a.__class__
na, da = a._numerator, a._denominator
nb, db = b._numerator, b._denominator
g = math.gcd(da, db)
if g == 1:
- return Fraction._from_coprime_ints(na * db + da * nb, da * db)
+ return cls._from_coprime_ints(na * db + da * nb, da * db)
s = da // g
t = na * (db // g) + nb * s
g2 = math.gcd(t, g)
if g2 == 1:
- return Fraction._from_coprime_ints(t, s * db)
- return Fraction._from_coprime_ints(t // g2, s * (db // g2))
+ return cls._from_coprime_ints(t, s * db)
+ return cls._from_coprime_ints(t // g2, s * (db // g2))
__add__, __radd__ = _operator_fallbacks(_add, operator.add)
def _sub(a, b):
"""a - b"""
+ cls = a.__class__
na, da = a._numerator, a._denominator
nb, db = b._numerator, b._denominator
g = math.gcd(da, db)
if g == 1:
- return Fraction._from_coprime_ints(na * db - da * nb, da * db)
+ return cls._from_coprime_ints(na * db - da * nb, da * db)
s = da // g
t = na * (db // g) - nb * s
g2 = math.gcd(t, g)
if g2 == 1:
- return Fraction._from_coprime_ints(t, s * db)
- return Fraction._from_coprime_ints(t // g2, s * (db // g2))
+ return cls._from_coprime_ints(t, s * db)
+ return cls._from_coprime_ints(t // g2, s * (db // g2))
__sub__, __rsub__ = _operator_fallbacks(_sub, operator.sub)
def _mul(a, b):
"""a * b"""
+ cls = a.__class__
na, da = a._numerator, a._denominator
nb, db = b._numerator, b._denominator
g1 = math.gcd(na, db)
@@ -824,13 +828,14 @@ def _mul(a, b):
if g2 > 1:
nb //= g2
da //= g2
- return Fraction._from_coprime_ints(na * nb, db * da)
+ return cls._from_coprime_ints(na * nb, db * da)
__mul__, __rmul__ = _operator_fallbacks(_mul, operator.mul)
def _div(a, b):
"""a / b"""
# Same as _mul(), with inversed b.
+ cls = a.__class__
nb, db = b._numerator, b._denominator
if nb == 0:
raise ZeroDivisionError('Fraction(%s, 0)' % db)
@@ -846,7 +851,7 @@ def _div(a, b):
n, d = na * db, nb * da
if d < 0:
n, d = -n, -d
- return Fraction._from_coprime_ints(n, d)
+ return cls._from_coprime_ints(n, d)
__truediv__, __rtruediv__ = _operator_fallbacks(_div, operator.truediv)
@@ -858,6 +863,7 @@ def _floordiv(a, b):
def _divmod(a, b):
"""(a // b, a % b)"""
+ cls = a.__class__
da, db = a.denominator, b.denominator
div, n_mod = divmod(a.numerator * db, da * b.numerator)
return div, Fraction(n_mod, da * db)
@@ -866,8 +872,9 @@ def _divmod(a, b):
def _mod(a, b):
"""a % b"""
+ cls = a.__class__
da, db = a.denominator, b.denominator
- return Fraction((a.numerator * db) % (b.numerator * da), da * db)
+ return cls((a.numerator * db) % (b.numerator * da), da * db)
__mod__, __rmod__ = _operator_fallbacks(_mod, operator.mod, False)
@@ -881,21 +888,22 @@ def __pow__(a, b, modulo=None):
"""
if modulo is not None:
return NotImplemented
+ cls = a.__class__
if isinstance(b, numbers.Rational):
if b.denominator == 1:
power = b.numerator
if power >= 0:
- return Fraction._from_coprime_ints(a._numerator ** power,
- a._denominator ** power)
+ return cls._from_coprime_ints(a._numerator ** power,
+ a._denominator ** power)
elif a._numerator > 0:
- return Fraction._from_coprime_ints(a._denominator ** -power,
- a._numerator ** -power)
+ return cls._from_coprime_ints(a._denominator ** -power,
+ a._numerator ** -power)
elif a._numerator == 0:
raise ZeroDivisionError('Fraction(%s, 0)' %
a._denominator ** -power)
else:
- return Fraction._from_coprime_ints((-a._denominator) ** -power,
- (-a._numerator) ** -power)
+ return cls._from_coprime_ints((-a._denominator) ** -power,
+ (-a._numerator) ** -power)
else:
# A fractional power will generally produce an
# irrational number.
@@ -923,15 +931,18 @@ def __rpow__(b, a, modulo=None):
def __pos__(a):
"""+a: Coerces a subclass instance to Fraction"""
- return Fraction._from_coprime_ints(a._numerator, a._denominator)
+ cls = a.__class__
+ return cls._from_coprime_ints(a._numerator, a._denominator)
def __neg__(a):
"""-a"""
- return Fraction._from_coprime_ints(-a._numerator, a._denominator)
+ cls = a.__class__
+ return cls._from_coprime_ints(-a._numerator, a._denominator)
def __abs__(a):
"""abs(a)"""
- return Fraction._from_coprime_ints(abs(a._numerator), a._denominator)
+ cls = a.__class__
+ return cls._from_coprime_ints(abs(a._numerator), a._denominator)
def __int__(a, _index=operator.index):
"""int(a)"""
@@ -977,10 +988,11 @@ def __round__(self, ndigits=None):
# See _operator_fallbacks.forward to check that the results of
# these operations will always be Fraction and therefore have
# round().
+ cls = self.__class__
if ndigits > 0:
- return Fraction(round(self * shift), shift)
+ return cls(round(self * shift), shift)
else:
- return Fraction(round(self / shift) * shift)
+ return cls(round(self / shift) * shift)
def __hash__(self):
"""hash(self)"""
diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py
index d1d2739856..0e57478f1d 100644
--- a/Lib/test/test_fractions.py
+++ b/Lib/test/test_fractions.py
@@ -851,7 +851,7 @@ def testMixedMultiplication(self):
self.assertTypedEquals(0.1 + 0j, (1.0 + 0j) * F(1, 10))
self.assertTypedEquals(F(3, 2) * DummyFraction(5, 3), F(5, 2))
- self.assertTypedEquals(DummyFraction(5, 3) * F(3, 2), F(5, 2))
+ self.assertTypedEquals(DummyFraction(5, 3) * F(3, 2), DummyFraction(5, 2))
self.assertTypedEquals(F(3, 2) * Rat(5, 3), Rat(15, 6))
self.assertTypedEquals(Rat(5, 3) * F(3, 2), F(5, 2))
@@ -881,7 +881,7 @@ def testMixedDivision(self):
self.assertTypedEquals(10.0 + 0j, (1.0 + 0j) / F(1, 10))
self.assertTypedEquals(F(3, 2) / DummyFraction(3, 5), F(5, 2))
- self.assertTypedEquals(DummyFraction(5, 3) / F(2, 3), F(5, 2))
+ self.assertTypedEquals(DummyFraction(5, 3) / F(2, 3), DummyFraction(5, 2))
self.assertTypedEquals(F(3, 2) / Rat(3, 5), Rat(15, 6))
self.assertTypedEquals(Rat(5, 3) / F(2, 3), F(5, 2))
@@ -927,7 +927,7 @@ def testMixedIntegerDivision(self):
self.assertTypedTupleEquals(divmod(-0.1, float('-inf')), divmod(F(-1, 10), float('-inf')))
self.assertTypedEquals(F(3, 2) % DummyFraction(3, 5), F(3, 10))
- self.assertTypedEquals(DummyFraction(5, 3) % F(2, 3), F(1, 3))
+ self.assertTypedEquals(DummyFraction(5, 3) % F(2, 3), DummyFraction(1, 3))
self.assertTypedEquals(F(3, 2) % Rat(3, 5), Rat(3, 6))
self.assertTypedEquals(Rat(5, 3) % F(2, 3), F(1, 3))
Has this already been discussed elsewhere?
No response given
Links to previous discussion of this feature:
No response