cpython
cpython copied to clipboard
Make Fraction more subclassing-friendly
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
CC @serhiy-storchaka
It is the same for builtin numeric types: int, float, complex. For subclasses, we cannot guarantee that constructor has expected signature and semantic.
>>> class I(int): pass
...
>>> type(I(1)+I(2))
<class 'int'>
Yes, I expected that argument, though I wasn't sure if it was the major reason to not have closure property for arithmetic in subclasses (other candidates: (a) legacy, (b) speed).
But I'm not sure if this is a strong argument, as in mentioned examples (and for Fraction) we have very specific base classes: they correspond to some well-defined mathematical objects. And we don't have a constraint, that instance methods (like arithmetic dunders) must use the class constructor. E.g. Fraction's methods use _from_coprime_ints() class method.
I think - mentioned closure property is more important than a hypothetical possibility to have an incompatible signature in some Fraction's child class. Ditto for other numeric types.
It is the same for builtin numeric types: int, float, complex. For subclasses, we cannot guarantee that constructor has expected signature and semantic.
Yes, this is our long-standing rule. I don't think it should be changed.
Recommend that this be closed.
Yes, this is our long-standing rule. I don't think it should be changed.
With this, subclassing any numeric types in the stdlib is useless, isn't?
It is possible, but it has limited use. For example, IntEnums are int subclasses with representation "by name". They serve as compatible replacement for integer constants. You can do arithmetic with them, but the result is bare int. bool is other similar example. It overrides | and & operators to return bool, but other operators return int.
If you implement something completely different, like gmpy2-based fractions, you rather create a class that implements all necessary protocols than subclass a builtin type.
If you implement something completely different, like gmpy2-based fractions
I don't think it's that different - actually Fraction supports this out of the box. The only problem is the constructor, that not casts integer literals to the gmpy2.mpz.
create a class that implements all necessary protocols than subclass a builtin type.
I.e. - not use the stdlib. Sounds odd.
Another example of customization: what if we want to override some arithmetic method to use a different algorithm?
Something similar was done for collections.abc.Set. I cannot say that I satisfied with that solution. If private method _from_iterable is a part of the protocol, then builtin set does not conform it. If it is not a part of protocol, where is the border between the protocol and the abstract class?
This is not a simple issue. Some classes allow to return and instance of the subclass, others always return an instance of the base class. There is no strict rule, it just happened that for numeric types it is a base class.
Now we have from_number() class methods, returning subclasses. Just another reason to not keep current constraints for arithmetic methods.
Another good example, where subclassing currently impossible is odd behavior of the int's true division. People e.g. in the SymPy - reinvent the whole int type just to get a rational number, instead of a float.