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