extend methods of decimal module
Steven D'Aprano
steve at pearwood.info
Thu Feb 27 22:15:36 EST 2014
On Thu, 27 Feb 2014 15:00:45 -0800, Mark H. Harris wrote:
> Decimal does not keep 0.1 as a floating point format (regardless of
> size) which is why banking can use Decimal without having to worry about
> the floating formatting issue... in other words, 0.0 is not stored in
> Decimal as any kind of floating value... its not rounded.... it really
> is Decimal('0.1').
I'm sorry, but that is incorrect. Decimal is a floating point format,
same as float. Decimal uses base 10, so it is a better fit for numbers we
write out in base 10 like "0.12345", but otherwise it suffers from the
same sort of floating point rounding issues as floats do.
py> a = Decimal("1.1e20")
py> b = Decimal("1.1e-20")
py> assert b != 0
py> a + b == a
True
In the case of 0.1 (I assume your "0.0" above was a typo), it is a
floating point value. You can inspect the fields' values like this:
py> x = Decimal("0.1")
py> x.as_tuple()
DecimalTuple(sign=0, digits=(1,), exponent=-1)
There's a sequence of digits, and an exponent that tells you where the
decimal point goes. That's practically the definition of "floating
point". In Python 3.2 and older, you can even see those fields as non-
public attributes:
py> x._int
'1'
py> x._exp
-1
(In Python 3.3, the C implementation does not allow access to those
attributes from Python.)
This is perhaps a better illustrated with a couple of other examples:
py> Decimal('1.2345').as_tuple()
DecimalTuple(sign=0, digits=(1, 2, 3, 4, 5), exponent=-4)
py> Decimal('1234.5').as_tuple()
DecimalTuple(sign=0, digits=(1, 2, 3, 4, 5), exponent=-1)
[...]
> The reason is that Decimal(.1) stores the erroneous float in the Decimal
> object including the float error for .1 and D(.1) works correctly
> because the D(.1) function in my dmath.py first converts the .1 to a
> string value before handing it to Decimal's constructor(s)
That *assumes* that when the user types 0.1 as a float value, they
actually intend it to have the value of 1/10 rather than the exact value
of 3602879701896397/36028797018963968. That's probably a safe bet, with a
number like 0.1, typed as a literal.
But how about this number?
py> x = 3832879701896397/36028797218963967
py> Decimal(x)
Decimal('0.10638378180104603176747701809290447272360324859619140625')
py> Decimal(str(x))
Decimal('0.10638378180104603')
Are you *absolutely* sure that the user intended x to have the second
value rather than the first? How do you know?
In other words, what you are doing is automatically truncating calculated
floats at whatever string display format Python happens to use,
regardless of the actual precision of the calculation. That happens to
work okay with some values that the user types in by hand, like 0.1. But
it is a disaster for *calculated* values.
Unfortunately, there is no way for your D function to say "only call str
on the argument if it is a floating point literal typed by the user".
But what you can do is follow the lead of the decimal module, and leave
the decision up to the user. The only safe way to avoid *guessing* what
value the caller wanted is to leave the choice of truncating floats up to
them. That's what the decimal module does, and it is the right decision.
If the user passes a float directly, they should get *exact conversion*,
because you have no way of knowing whether they actually wanted the float
to be truncated or not. If they do want to truncate, they can pass it to
string themselves, or supply a string literal.
--
Steven
More information about the Python-list
mailing list