classification
Title: Include Decimal's in numbers.Real
Type: Stage: resolved
Components: Documentation, Library (Lib) Versions: Python 3.10
process
Status: closed Resolution: rejected
Dependencies: Superseder:
Assigned To: docs@python Nosy List: Sergey.Kirpichev, docs@python, mark.dickinson, oscarbenjamin, rhettinger, tim.peters
Priority: normal Keywords:

Created on 2021-03-23 06:30 by Sergey.Kirpichev, last changed 2021-04-23 06:26 by Sergey.Kirpichev. This issue is now closed.

Messages (11)
msg389372 - (view) Author: Sergey B Kirpichev (Sergey.Kirpichev) * Date: 2021-03-23 06:30
Commit 82417ca9b2 includes Decimal's in the numbers tower, but only as an implementation of the abstract numbers.Number.  The mentioned reason is "Decimals are not interoperable with floats" (see comments in the numbers.py as well), i.e. there is no lossless conversion (in general, in both directions).

While this seems to be reasonable, there are arguments against:

1) The numbers module docs doesn't assert there should be a lossless conversion for implementations of same abstract type.  (Perhaps, it should.)  This obviously may be assumed for cases, where does exist an exact representation (integers, rationals and so on) - but not for real numbers (or complex), where representations are inexact (unless we consider some subsets of real numbers, e.g. some real finite extension of rationals - I doubt such class can represent numbers.Real).

(Unfortunately, the Scheme distinction of exact/inexact was lost in PEP 3141.)

2) By same reason, I think, neither binary-based multiprecision arithmetics package can represent numbers.Real: i.e. gmpy2.mpfr, mpmath.mpf and so on.  (In general, there is no lossless conversion float's, in both directions.)

3) That might confuse users (why 10-th base arbitrary precision floating point arithmetic can't represent real numbers?).

4) Last, but not least, even some parts of stdlib uses both types in an interoperable way, e.g. Fraction constructor:
    elif isinstance(numerator, (float, Decimal)):
        # Exact conversion
        self._numerator, self._denominator = numerator.as_integer_ratio()
        return self
msg389374 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2021-03-23 07:35
We don't have a choice here.  Operations between decimals and floats raise a TypeError.  So, we can't register Decimal as a Real; otherwise, static type checking wouldn't be able to flag the following as invalid:

    def add(a: Real, b: Real) -> Real:
        return a + b

    a: Real = Decimal('1.1')
    b: Real = 2.2
    print(add(a, b))

This gives:

    Traceback (most recent call last):
      File "/Users/raymond/Documents/tmp.py", line 10, in <module>
        print(add(a, b))
      File "/Users/raymond/Documents/tmp.py", line 6, in add
        return a + b
    TypeError: unsupported operand type(s) for +: 'decimal.Decimal' and 'float'

Almost the whole point of static checking is early detection of these problems so we won't encounter the TypeError at runtime.

P.S.  With respect to #4, we've harmonized the APIs as much as we sensibly can.  That allows some code to be more polymorphic as long at the type is consistent throughout.  That is much different than freely mixing floats and decimals in the direct interactions.
msg389376 - (view) Author: Sergey B Kirpichev (Sergey.Kirpichev) * Date: 2021-03-23 08:21
> Operations between decimals and floats raise a TypeError.

I saw this)  And as I said, I assume, the reason is: there is no lossless conversion to float's (and vice verse).

If so (point 2), neither multiple-precision type (e.g. gmpy2.mpfr) can subclass from the numbers.Real (there can be different precisions, different bases) and that sounds too restrictive.

From the mathematician point of view, both built-in float's and Decimal's could be viewed as (inexact!) representations for real numbers.  But if _any_ such representations, conforming the numbers abc must be lossless converted to each other - that might be a documentation issue.

> P.S.  With respect to #4, we've harmonized the APIs as much as we sensibly can.

That was very minor, yes.  Something like try-except could be used here, trying as_integer_ratio().
msg389380 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2021-03-23 10:21
> I assume, the reason is: there is no lossless conversion to float's (and vice verse).

No, I don't think that's the reason (and in fact we _do_ have lossless conversion of floats to Decimal instances). IMO, the reasons are:

- it's not obvious what the *type* of the result of some_float + some_other_decimal should be, and

- it seems rather likely that any attempt to combine a float and a Decimal instance in this way is a bug, or at least something that hasn't been fully thought through by the developer, so we force the developer to make an explicit conversion

For historical discussions, see #1682.
msg389386 - (view) Author: Sergey B Kirpichev (Sergey.Kirpichev) * Date: 2021-03-23 12:43
On Tue, Mar 23, 2021 at 10:21:50AM +0000, Mark Dickinson wrote:
> Mark Dickinson <dickinsm@gmail.com> added the comment:
> > I assume, the reason is: there is no lossless conversion to float's (and vice verse).
> and in fact we _do_ have lossless conversion of floats to Decimal instances

Indeed, context precision doesn't affect this.  (But still, reversed
conversion is inexact in general).

> - it's not obvious what the *type* of the result of some_float + some_other_decimal should be

Seems so, for a static typing.  But Python is a dynamically typed
language, isn't?

  >>> import gmpy2
  >>> gmpy2.mpfr('1.0') + 1.0
  mpfr('2.0')
  >>> 1.0 + gmpy2.mpfr('1.0')
  mpfr('2.0')

(ditto mpmath)

> - it seems rather likely that any attempt to combine a float and
> a Decimal instance in this way is a bug, or at least something that
> hasn't been fully thought through by the developer, so we
> force the developer to make an explicit conversion

Maybe it's ok for base-2 multiprecision arithmetics, as in the
example above.

Maybe not.  But in this case, if interoperability with float's (or any other
implementation for numbers.Real) is a requirement for any
numbers.Real-derived class - that should be documented.  Perhaps, then
there are bugs in mpmath/gmp2, which do claim they implement Real type:
i.e. either they should't implement automatic conversion or don't claim
they do implement numbers.Real.

> For historical discussions, see #1682.

Thank you, I'll look into.
msg389387 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2021-03-23 13:03
> Seems so, for a static typing.  But Python is a dynamically typed
> language, isn't?

I think we're talking at cross purposes. Static and dynamic typing have nothing to do with this.

What do you think the result of `1.0 + Decimal(1)` should be, and more importantly why? Possible options are:

- Decimal('2')
- 2.0 (a float)
- a `TypeError` (as now)
- some kind of horrible user-configurable-global-state-dependent answer

Bear in mind that you have to pick a behaviour that's a good default choice for all potential application domains, and that's *hard*. ("In the face of ambiguity ...", and all that.)
msg389400 - (view) Author: Sergey B Kirpichev (Sergey.Kirpichev) * Date: 2021-03-23 18:20
On Tue, Mar 23, 2021 at 01:03:47PM +0000, Mark Dickinson wrote:
> What do you think the result of `1.0 + Decimal(1)` should be, and
> more importantly why? Possible options are:
> 
> - Decimal('2')
> - 2.0 (a float)
> - a `TypeError` (as now)
> - some kind of horrible user-configurable-global-state-dependent answer

Decimal, with a some kind of "horrible
user-configurable-global-state-dependent answer" (Decimal
context): reverse conversion might be inexact.
Same, in principle, holds for 2-base multiprecision arithmetic
types like gmpy2.mpfr and mpmath.mpf.  "More powerfull data type,
claiming it implements numbers.Real - should know better."
That's the first option.

Maybe I (and authors of mentioned above packages) - do miss something
important.  (Oh, count on SageMath too.)
But do we have other examples of numbers.Real implementations (or
claiming to be such)?  If the numbers.Real does mean something like "only
python's builtin floats, but maybe with a different multiplication
algorithm" - that's hardly something that people may expect from
the docs.  Real numbers have a very specific mathematical meaning and
things like mpmath's mpf or Decimal fit this.

> Bear in mind that you have to pick a behaviour that's a good default
> choice for all potential application domains, and that's *hard*.

I think, that TypeError (a second option) might make sense too.  I'm not
sure that different implementations of numbers.Real must be interoperable (i.e.
without explicit conversions).  Such requirement clearly does make sense for
exact data types in the numerical tower (i.e. different
numbers.Rational implementations).

So, this implementation of the numbers tower:
  int (Integral) - Fraction (Rational) - float (Real) - complex (Complex)
doesn't look "more correct", than this:
  gmpy2.mpz - gmpy2.mpq - gmpy2.mpfr - gmpy2.mpc
regardless on how do "inexact" data types (e.g. float vs mpfr)
interoperate.  Same may be for the Decimal (but this is not a full tower):
  int - Fraction - Decimal.
msg391173 - (view) Author: Sergey B Kirpichev (Sergey.Kirpichev) * Date: 2021-04-16 09:20
Probably, this thread
https://mail.python.org/archives/list/python-ideas@python.org/thread/KOE3MQ5NSMGTLIH6IHAQWTIOELXG4AFQ/
is relevant here.  I would appreciate Oscar's feedback on this issue.
msg391183 - (view) Author: Oscar Benjamin (oscarbenjamin) * Date: 2021-04-16 11:08
I've never found numbers.Real/Complex to be useful. The purpose of the ABCs should be that they enable you to write code that works for instances of any subclass but in practice writing good floating point code requires knowing something e.g. the base, precision, max exponent etc of the type. Also many implementations like Decimal have contexts and rounding control etc that need to be used and the ABC gives no way to know that or to do anything with it.

The main thing that is useful about the Rational/Integer ABCs is that they define the numerator and denominator attributes which makes different implementations interoperable by providing exact conversion. If Real was presumed to represent some kind of floating point type then an analogous property/method would be something that can deconstruct the object in an exact way like:

mantissa, base, exponent = deconstruct(real)

You would also need a way to handle nan, inf etc. Note that as_integer_ratio() is not suitable because it could generate enormous integers unnecessarily e.g. Decimal('1E+100000000').as_integer_ratio().

Instead the Real ABC only defines conversion to float. That's useful in the sense that you can write code for float and pass in some other floating point type and have everything reduce to float. You don't need an ABC for that though because __float__ does everything. In practice most alternate "real" number implementations exist precisely to be better than float in some way by either having greater range/precision or a different base but anything written for the Real ABC is essentially reduced to float as a lowest common (inexact) denominator.
msg391353 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2021-04-19 03:38
Considering Oscar's response, Mark's comments, and prior discussions, we should close this.  No strong use cases have emerged that would warrant overturning the long-standing prior decisions on this topic.
msg391657 - (view) Author: Sergey B Kirpichev (Sergey.Kirpichev) * Date: 2021-04-23 06:26
Well, probably everyone else agree with Raymond.  Yet, I'll
try to clarify few things.

On Mon, Apr 19, 2021 at 03:38:29AM +0000, Raymond Hettinger wrote:
> No strong use cases have emerged that would warrant overturning
> the long-standing prior decisions on this topic.

How about other multiprecision types in external libs, i.e. mpmath.mpf
or gmpy2.mpfr?  Probably, these shouldn't be Real's as well, isn't?
In this way, the whole numbers tower above Rational class is more or
less useless, as mentioned by Oscar: Real ABC is essentially reduced to
float and Complex ABC - to complex...

Raymond, I won't object your current decision for Decimal.  But do
you think - there is no documentation issues with the numbers module,
related to Real/Complex?

The module doesn't document, for example, that R1 + R2 is expected to
work if R1 and R2 are both Reals (but different implementations).  I'm not
sure if this is a sane design decision (as this will restrict Real ABC
just to float's),  but if so - it must be documented. (comments are internal
documentation, isn't?).  It's not obvious.  (The proof e.g. is that
Decimal vs Real issue was questioned several times by different people.)
History
Date User Action Args
2021-04-23 06:26:56Sergey.Kirpichevsetmessages: + msg391657
2021-04-19 03:38:29rhettingersetstatus: open -> closed
resolution: rejected
messages: + msg391353

stage: resolved
2021-04-16 11:08:05oscarbenjaminsetmessages: + msg391183
2021-04-16 09:20:56Sergey.Kirpichevsetnosy: + oscarbenjamin
messages: + msg391173
2021-03-23 21:17:11rhettingersetnosy: + tim.peters
2021-03-23 18:20:17Sergey.Kirpichevsetmessages: + msg389400
2021-03-23 13:03:47mark.dickinsonsetmessages: + msg389387
2021-03-23 12:43:22Sergey.Kirpichevsetmessages: + msg389386
2021-03-23 10:21:49mark.dickinsonsetnosy: + mark.dickinson
messages: + msg389380
2021-03-23 08:21:56Sergey.Kirpichevsetmessages: + msg389376
2021-03-23 07:35:42rhettingersetmessages: - msg389373
2021-03-23 07:35:30rhettingersetmessages: + msg389374
2021-03-23 07:28:11rhettingersetnosy: + rhettinger
messages: + msg389373
2021-03-23 06:30:40Sergey.Kirpichevcreate