Just to make it more clear what we’re talking about, could you post a link to that code?
In my view, “backwards compatibility” is about observable functionality. A good proxy for this is: it shouldn’t break any testsuites. By this definition, my proposal can be implemented in a backwards compatible way.
What’s happening here is that my proposal is adding a bug to your code. The fact is and remains that your code (unchanged) doesn’t support SageMath rationals. Right now, your code is not to blame for this. But with the proposal, your code might be blaimed.
No, it’s unpublished personal code. To avoid misunderstanding, I’m not talking about a production system that will break here - obviously I won’t suddenly be adding SageMath rationals to such code. This is a small personal project, mostly experimental (it’s playing about with continued fractions) that could well languish for a while and then I’ll pick it up and run it again. I probably also have some data analysis code from a while back that uses fractions and freely expects to use .numerator
etc. All I’m really saying is that I’m a relatively frequent user of fractions in my code, and I can’t assess the impact on my code from the partial proposal you’ve posted this far.
Ultimately, though, I don’t care at this point. Until I see a full proposal, I’m just anticipating problems. Once you have the proposal written up, then I’ll look at it and decide whether I’m comfortable with the breakage (and whether or not I want to argue with you over what counts as “breakage”, if that’s necessary).
Ok, here is a more complete proposal:
-
Define a new special method
__ratio__
with the following meaning:q.__ratio__()
must be a 2-tuple(n, d)
of integers such thatq * d == n
. The objectsn
andd
don’t have to be Python ints, but they must support__index__
. -
Within the standard library,
int
,fractions.Fraction
,float
anddecimal.Decimal
would support__ratio__
. For the latter two, this would be an alias ofas_integer_ratio
. Note that__ratio__
is therefore not limited tonumbers.Rational
, it can be used also for exact arithmetic with floating point numbers. This may be useful for thetime
module, see bpo-35707. -
The specification of
numbers.Rational
would be changed to say that rationals must have such a__ratio__
method. Thenumerator
anddenominator
properties are no longer required (but the expectation is that existing classes will keep them). -
numbers.Rational
would have a default implementation of__ratio__
returning(self.numerator, self.denominator)
. Note that this automatically makes__ratio__
work forfractions.Fraction
and all other existing classes inheriting fromnumbers.Rational
. -
A helper function
operator.ratio(x)
is added, returningx.__ratio__()
but falling back to(x.numerator, x.denominator)
. This is recommended over calling__ratio__
manually. If it’s deemed useful, also a C API function will be added. -
The constructor for
fractions.Fraction
would useoperator.ratio()
.
As far as I can see, this proposal is backwards compatible in the sense that all functionality that used to work still works. Nevertheless, existing code should be changed to support the new protocol:
-
Classes registering as
numbers.Rational
without actually inheriting should implement__ratio__
. -
Code checking for
numbers.Rational
and accessing thenumerator
/denominator
properties should useoperator.ratio()
instead.
I don’t consider this backwards compatible. Every user of .numerator etc. would have to be changed. And what’s the point? So SageMath numbers can claim compatibility with PEP 3141? Even the stdlib’s decimal module doesn’t claim to be compatible with the numbers API. I really don’t see the point of making such a change. If you need to pass a SageMath number to some API that expects the numbers API you’ll just have to convert at the boundary.
To use RFC 2119, it’s not REQUIRED for such code to change but it is RECOMMENDED (nothing that used to work will break if code doesn’t change). The first is obviously not backwards compatible, the second is in a gray zone depending on how strictly you define backwards compatibility.
The real goal is interoperability with fractions.Fraction
and currently PEP 3141 is the only means to that end. Think of it this way: we have __index__
for converting to a Python int, __float__
to a float and __complex__
to a complex. But we’re missing a special method for fractions.
And what real-world problem does that solve?
There are various mathematical packages that use fractions.Fraction
which are actually used by people to do real work. For example, realalg to name just one.
Using such packages inside SageMath is more difficult than it should be because Fraction
does not support SageMath integers/rationals. Of course, one can do explicit conversions but that’s not convenient. For integers, __index__
was invented to solve this problem.
It seems now that Python is converging towards using as_integer_ratio
for rationals. The only part that’s missing is actually using as_integer_ratio
in the Fraction
constructor, see either PR 15327 or PR 15329.
Sounds good.