# 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.