Revise true division for ints? Lets return (optionally) a Fraction!

This is at some extent a split of from recent topic, proposing literals for Fraction’s, as above proposal requires also more deep integration of fractions to the core language.

The feature, which (I think) hatedislike most people doing math in Python (especially multiple precision arithmetic or symbolic math) is true division of integers:

Python 3.13.0rc1+ (heads/3.13:bd29ce8509, Aug 29 2024, 15:30:15) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 1/3
0.3333333333333333

I think we can “fix” this without breaking backward compatibility. Consider the gmpy2:

>>> import gmpy2
>>> gmpy2.get_context().rational_division
False
>>> gmpy2.mpz(1)/3
mpfr('0.33333333333333331')
>>> gmpy2.get_context().rational_division = True
>>> gmpy2.mpz(1)/3
mpq(1,3)

So, why not adopt similar approach for CPython? Recently it was added interface to control integer string conversion length limitation. Interface like this could be used to control behaviour for integer division.

Actually, there is one incompatible change: it’s constant folding in the CPython, as currently:

>>> dis.dis('1/3')
  0           0 RESUME                   0

  1           2 RETURN_CONST             0 (0.3333333333333333)

But I think that such optimizations are implementation details of the CPython and might be changed.

I haven’t played with this idea a lot and there is no POC implementation, but required changes aren’t look too big and/or complex.

2 Likes

People should face 0.3333333333333333. Division is not an operation of integers.

I think / should still mean float result, // Euclidean division.

A third notation could be used to deliberately and explicitly ask for this new something-else.

To me this (if it means doing it to / without a very explicit indication that one wants to return something new Fraction) is as much a -1 as the change done to sum of float, which doesn’t sum float anymore.

You are 35 years too late. This was one of mistakes of ABC that was fixed in Python.

Read The History of Python: Early Language Design and Development.

5 Likes

Division is an operation for integers and can be defined in a way that is not Euclidean division and does not use floats. The python-flint library uses exact division:

>>> from flint import fmpz
>>> fmpz(6) / fmpz(3)
2
>>> fmpz(6) / fmpz(5)
...
DomainError: fmpz division is not exact

The fact that not all integers divide is not necessarily a reason to use rationals or floats. In many situations it is better to stay in a well defined domain (e.g. integers) rather than implicitly extending to a larger domain like rationals or floats.

1 Like

I mean the exact opposite. That because it is not an operation of integers, there is no type of the result that is the most desirable. Therefore, it is better for the expressions to have explicit indication of what one intends to compute.

You added more examples to what I would expect to see, writing fmpz(6) / fmpz(3) if that type is what one intends.

That, as opposed to having / return an exact type, when some global config is set or something like that.

Something I don’t think is good is making it easy the sequence of ideas:

  1. 0.3333333333333333 floats are weird. Their finite precision is a deficiency.”
  2. “Turn this switch and all operations are exact.”

A global setting like this would affect library code, not just your own. You’d break any library using the result of a division in a way that isn’t Fraction vs float agnostic.

2 Likes

Yes, you are right and that is exactly what python-flint does:

In [1]: from flint import fmpz, fmpq, arb

In [2]: fmpz(6)/fmpz(5)  # integers
...
DomainError: fmpz division is not exact

In [3]: fmpq(6)/fmpq(5)  # rationals
Out[3]: 6/5

In [4]: arb(6)/arb(5) # real approximate
Out[4]: [1.20000000000000 +/- 2.67e-16]

Probably not but it would nice to be able to switch it on on a per-module basis somehow sort of like:

from __fractions__ import division

I’m sure no one wants to make that though.

One thing that could help with this and a number of related issues for mathematical code is to have macros so you can control this stuff at the statement level:

# Implicitly defines symbol x
# Treats 1/2 as a fraction
# Handles juxtaposed multiplication
# Uses proper ^ for exponentiation
p = sym! x^2 + 2x + 1/2

I’ve read that text before, thanks). And I’m not suggesting to revisit that decision.

But hardly it was a mistake. Certainly, the different decision, taken in Mathematica, Maple, lisps - does make sense for me. You can take look on SymPy/Diofant tests to see how often used something like Rational(1, 3) (or S(1)/3 in SymPy) to workaround current behaviour of true division for integers.

So, you are suggesting implicitly convert them to floats, to get float result. (This is not what exactly happens under the hood, but - a some justification of why float result is allowed.) But I suggest an implicit conversion to Fraction’s instead.

Yep, that might be an issue. (Just as with integer string conversion limitation.) But it’s not so easy to provide a practical example (perhaps, only numbers.Rational.__float__ method in the stdlib): fractions have same set of arithmetic operations defined, have __float__ dunder, etc. Missing methods: hex/fromhex and is_integer.

That might fix mentioned above (possible?) problems in used libraries from turning on the global switch. Yet I think that most applications, that might have benefits from new behaviour (like SymPy) - want a global switch.

Some context manager could be used to override setting more locally.

I wanted to say we can define semantics that make sense, but it’s already implemented since 3.12:

>>> from fractions import Fraction
>>> Fraction(1).is_integer()
True
>>> Fraction(1, 2).is_integer()
False
1 Like