New submission from Sergey B Kirpichev skirpichev@gmail.com:
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
---------- assignee: docs@python components: Documentation, Library (Lib) messages: 389372 nosy: Sergey.Kirpichev, docs@python priority: normal severity: normal status: open title: Include Decimal's in numbers.Real versions: Python 3.10
_______________________________________ Python tracker report@bugs.python.org https://bugs.python.org/issue43602 _______________________________________
Raymond Hettinger raymond.hettinger@gmail.com added the comment:
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 to avoid these TypeErrors at runtime
---------- nosy: +rhettinger
_______________________________________ Python tracker report@bugs.python.org https://bugs.python.org/issue43602 _______________________________________
Raymond Hettinger raymond.hettinger@gmail.com added the comment:
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.
----------
_______________________________________ Python tracker report@bugs.python.org https://bugs.python.org/issue43602 _______________________________________
Change by Raymond Hettinger raymond.hettinger@gmail.com:
---------- Removed message: https://bugs.python.org/msg389373
_______________________________________ Python tracker report@bugs.python.org https://bugs.python.org/issue43602 _______________________________________
Sergey B Kirpichev skirpichev@gmail.com added the comment:
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().
----------
_______________________________________ Python tracker report@bugs.python.org https://bugs.python.org/issue43602 _______________________________________
Mark Dickinson dickinsm@gmail.com added the comment:
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.
---------- nosy: +mark.dickinson
_______________________________________ Python tracker report@bugs.python.org https://bugs.python.org/issue43602 _______________________________________
Sergey B Kirpichev skirpichev@gmail.com added the comment:
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.
----------
_______________________________________ Python tracker report@bugs.python.org https://bugs.python.org/issue43602 _______________________________________
Mark Dickinson dickinsm@gmail.com added the comment:
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.)
----------
_______________________________________ Python tracker report@bugs.python.org https://bugs.python.org/issue43602 _______________________________________
Sergey B Kirpichev skirpichev@gmail.com added the comment:
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.
----------
_______________________________________ Python tracker report@bugs.python.org https://bugs.python.org/issue43602 _______________________________________
Change by Raymond Hettinger raymond.hettinger@gmail.com:
---------- nosy: +tim.peters
_______________________________________ Python tracker report@bugs.python.org https://bugs.python.org/issue43602 _______________________________________
Sergey B Kirpichev skirpichev@gmail.com added the comment:
Probably, this thread https://mail.python.org/archives/list/python-ideas@python.org/thread/KOE3MQ5... is relevant here. I would appreciate Oscar's feedback on this issue.
---------- nosy: +oscarbenjamin
_______________________________________ Python tracker report@bugs.python.org https://bugs.python.org/issue43602 _______________________________________
Oscar Benjamin oscar.j.benjamin@gmail.com added the comment:
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.
----------
_______________________________________ Python tracker report@bugs.python.org https://bugs.python.org/issue43602 _______________________________________
Raymond Hettinger raymond.hettinger@gmail.com added the comment:
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.
---------- resolution: -> rejected stage: -> resolved status: open -> closed
_______________________________________ Python tracker report@bugs.python.org https://bugs.python.org/issue43602 _______________________________________
Sergey B Kirpichev skirpichev@gmail.com added the comment:
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.)
----------
_______________________________________ Python tracker report@bugs.python.org https://bugs.python.org/issue43602 _______________________________________