[Tutor] Floating Point Craziness
Steven D'Aprano
steve at pearwood.info
Sun Jun 12 19:13:05 CEST 2011
Ryan Strunk wrote:
> Hi everyone,
> I'm designing a timeline. When the user presses the right arrow, 0.1 is
> added to the current position. The user can add events to the timeline, and
> can later scroll back across those events to see what they are. But
> something I absolutely don't understand is happening:
> I used the program to place events at 1.1, 2.1, and 3.1. Here is the end of
> the debug output for arrowing to 3.1 and placing the event:
> position = 2.7
> position = 2.8
> position = 2.9
> position = 3.0
> position = 3.1
> event placed.
> Everything appears straight forward.
It only *appears* straight forward, it actually isn't. That's because
the printed output is rounded so as to look nice. Compare:
>>> x = 1/10.0
>>> print(x) # displayed as nicely as possible
0.1
>>> print('%.25f' % x) # display 25 decimal places
0.1000000000000000055511151
For speed and simplicity, floats are stored by the computer using
fractional powers of two. That's fine for numbers which are powers of
two (1/2, 1/4, 1/256, and sums of such) but numbers that humans like to
use tend to be powers of 10, not 2.
Unfortunately, many common fractions cannot be written exactly in
binary. You're probably familiar with the fact that fractions like 1/3
cannot be written exactly in decimal:
1/3 = 0.33333333... goes on forever
The same is true for some numbers in binary:
1/10 in decimal = 1/16 + 1/32 + 1/256 + ...
also goes on forever. Written in fractional bits:
1/10 decimal = 0.00011001100110011... in binary
Since floats can only store a finite number of bits, 1/10 cannot be
stored exactly. It turns out that the number stored is a tiny bit larger
than 1/10:
>>> Fraction.from_float(0.1) - Fraction(1)/10
Fraction(1, 180143985094819840)
Rounding doesn't help: 0.1 is already rounded as close to 1/10 as it can
possibly get.
Now, when you print the dictionary containing those floats, Python
displays (almost) the full precision:
>>> print {1: 0.1}
{1: 0.10000000000000001}
In newer versions of Python, it may do a nicer job of printing the float
so that the true value is hidden. E.g. in Python 3.1 I see:
>>> print({1: 0.1})
{1: 0.1}
but don't be fooled: Python's print display is pulling the wool over
your eyes, the actual value is closer to 0.10000000000000001.
One consequence of that is that adding 0.1 ten times does not give 1
exactly, but slightly less than 1:
>>> x = 0.1
>>> 1 - sum([x]*10)
1.1102230246251565e-16
(P.S. this is why you should never use floats for currency. All those
missing and excess fractions of a cent add up to real money.)
To avoid this, you can:
* Upgrade to a newer version of Python that lies to you more often, so
that you can go on living in blissful ignorance (until such time as you
run into a more serious problem with floats);
* Use the fraction module, or the decimal module, but they are slower
than floats; or
* Instead of counting your timeline in increments of 0.1, scale
everything by 10 so the increment is 1.
That is, instead of:
2.0 2.1 2.2 2.3 ...
you will have
20 21 22 23 24 ...
* Or you just get used to the fact that some numbers are not exact in
floating point.
--
Steven
More information about the Tutor
mailing list