PEP 3141: __ratio__ instead of numerator/denominator

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:

  1. Define a new special method __ratio__ with the following meaning: q.__ratio__() must be a 2-tuple (n, d) of integers such that q * d == n. The objects n and d don’t have to be Python ints, but they must support __index__.

  2. Within the standard library, int, fractions.Fraction, float and decimal.Decimal would support __ratio__. For the latter two, this would be an alias of as_integer_ratio. Note that __ratio__ is therefore not limited to numbers.Rational, it can be used also for exact arithmetic with floating point numbers. This may be useful for the time module, see bpo-35707.

  3. The specification of numbers.Rational would be changed to say that rationals must have such a __ratio__ method. The numerator and denominator properties are no longer required (but the expectation is that existing classes will keep them).

  4. numbers.Rational would have a default implementation of __ratio__ returning (self.numerator, self.denominator). Note that this automatically makes __ratio__ work for fractions.Fraction and all other existing classes inheriting from numbers.Rational.

  5. A helper function operator.ratio(x) is added, returning x.__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.

  6. The constructor for fractions.Fraction would use operator.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:

  1. Classes registering as numbers.Rational without actually inheriting should implement __ratio__.

  2. Code checking for numbers.Rational and accessing the numerator/denominator properties should use operator.ratio() instead.

1 Like

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?

2 Likes

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.

3 Likes

Sounds good.