On Thu, 15 Oct 2020 at 04:22, Guido van Rossum guido@python.org wrote:

This all seems a pretty artificial argument.

In plain English, "1/3" is not exactly representable in decimal form, but something like 0.1111111111111111111111111111 * 0.22222222222222222222222222 *is* (assuming the inputs are what they appear).

Since the spec clearly means to say "exactly representable in the (current or given) context's precision" that phrase (or something non-empty like it) should just be added to the docs and the argument is over.

Maybe the argument isn't productive but I think that there is a reasonable feature request in this.

The decimal module currently has a way to trap whenever something inexact occurs e.g.:

from decimal import * getcontext().traps[Inexact] = True Decimal('1') / Decimal('3')

Traceback (most recent call last): File "<stdin>", line 1, in <module> decimal.Inexact: [<class 'decimal.Inexact'>]

That's useful for ensuring that a calculation cannot silently propagate rounding errors: if it succeeds the result is exact.

In the case of 1/3 no finite level of precision can give an exact result but in other cases it is possible that increasing the precision would:

d = Decimal('1' * 20) d*d

Traceback (most recent call last): File "<stdin>", line 1, in <module> decimal.Inexact: [<class 'decimal.Inexact'>]

getcontext().prec = 50 d*d

Decimal('123456790123456790120987654320987654321')

The useful feature that is not available from the decimal module is a way to say: use enough digits to give an exact result if at all possible but otherwise raise an error (or perhaps round the result depending on traps).

There are a couple of obvious strategies for trying to find that number of digits that have been suggested above in this thread. One approach is to trap the inexact flag and increase the precision if needed:

while True: try: calculation() except Inexact: getcontext().prec *= 2

The problem is that for something like 1/3 this is an infinite loop that will consume all the memory in the machine. Hopefully you'll get something like MemoryError but this can also just bork everything. Of course we can put limits on the loop but we don't actually need a high precision to conclude that an exact division will not be possible in the case of 1/3.

Another approach is just to use the maximum precision specified by the module but that also leads to possible borkage:

getcontext().prec = MAX_PREC Decimal('1') / Decimal('3')

Python(74959,0x10b4cd5c0) malloc: can't allocate region *** mach_vm_map(size=421052631578947584) failed (error code=3) Python(74959,0x10b4cd5c0) malloc: *** set a breakpoint in malloc_error_break to debug Traceback (most recent call last): File "<stdin>", line 1, in <module> MemoryError

A better approach is to think carefully about each individual operation and bound the number of digits that could be needed for any possible exact result:

from decimal import *

def exact_divide(a, b): num_digits = lambda d: len(d.as_tuple().digits) digits_bound = num_digits(a) + 4 * num_digits(b) ctx = getcontext().copy() ctx.traps[Inexact] = True ctx.prec = digits_bound return ctx.divide(a, b)

I haven't thought too hard about corner cases but I think that something like num_digits(a) + 4 * num_digits(b) bounds the possible number of digits for any possible exact a/b. After cancelling the gcd of the mantissas we can only divide exactly by 2, 5 or some combination of those. Each factor of 2 or 5 in the divisor requires one extra digit so the worst case is dividing by a power of 2 and 10**n < (2**4)**n. So I think the number of extra digits needed is no more than 4 times the number of digits in the divisor. That gives:

exact_divide(Decimal(1), Decimal(8))

Decimal('0.125')

a = 123456789 ** 10 exact_divide(Decimal(a**2), Decimal(a))

Decimal('822526259147102579504761143661535547764137892295514168093701699676416207799736601')

exact_divide(Decimal(1), Decimal(3))

Traceback (most recent call last): File "<stdin>", line 1, in <module> File "asd.py", line 10, in exact_divide return ctx.divide(a, b) decimal.Inexact: [<class 'decimal.Inexact'>]

Of course some of these operations could lead to a lot of memory usage like

exact_add(Decimal('1e10000000000000'), Decimal('1'))

but there can also be ways to limit the maximum precision that can be used by exact_add without requiring the working precision itself to be at that level for *everything* (including 1/3).

The standards on which the decimal module is based do not require that there be functions for doing this as they are based on the idea that the precision is not necessarily configurable. Python's implementation does provide configurable precision though and also makes it possible to represent an individual decimal object with any number of digits regardless of the active context.

It would be useful in some cases to have functions in the decimal module for performing exact-if-possible operations like this. Potentially it could make sense as a special context although I don't know how easy it would be to implement like that.

Also an obvious missing feature is the ability to convert from Fraction to Decimal exactly (when the result can be exact but raising otherwise). In that case we pretty much know that if the Fraction can be represented within the constraints of memory then any corresponding exact Decimal can be as well. The reverse is not the case even though it is possible to convert exactly from Decimal to Fraction:

Fraction(Decimal('1e100000000000')) # this is dangerous

^C

-- Oscar