On Thu, 15 Oct 2020 at 04:22, Guido van Rossum <gu...@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
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/VOT4SW2MMNFFC6GAF5TIRQ7ZEWJG7HH3/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to