2

I run:

print(0.1)
print(0.2)
print(0.3)
print(0.4)
print(0.5)
print(0.5-0.4)
print(0.4-0.3)
print(0.3-0.2)
print(0.2-0.1)

It shows:

0.1
0.2
0.3
0.4
0.5
0.09999999999999998
0.10000000000000003
0.09999999999999998
0.1

Why print number directly is fine, but use of the operator - will get unexpected value?

I have checked: Is floating point math broken?

If 0.5-0.4 could not be saved in hardware precisely, why just print(0.1) will show 0.1 not 0.10000000000xxx?

Maybe more clear statement is:

In my understanding, python should print(0.1) to be 0.10000000000xxx, and 0.5-0.4 should equal to 0.4-0.3 and equal to 0.3-0.2, right?

Why python could "Python keeps the number of digits manageable by displaying a rounded value"? If python wants to print(0.1) to be 0.1, it should treat 0.4-0.3 as 0.1 well, right? Why python want us to be confusion on this kind of topics?

For even more precise statement that this problem is "not" really argue that 0.5-0.4 != 0.4-0.3. My main problem is print(0.1)= 0.1 not 0.10000000000xxxx, why python could do this kind of action? It will let us very confusion with the "Is floating point math broken?"

sam
  • 2,049
  • 3
  • 20
  • 27

1 Answers1

5

Because the numbers are not equal. Consider the following, when you pass a float directly to Decimal to get a better representation of what is going on:

>>> from decimal import Decimal
>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> Decimal(0.5 - 0.4)
Decimal('0.09999999999999997779553950749686919152736663818359375')

Notice, that the actual floating point value you get from the literal 0.1 is different than the result of the subtraction of floats that are created from the literals 0.5 and 0.4. This is because the two literals also can have an error (note, 0.5 can be represented exactly, because it can be expressed as a power of two).

>>> Decimal(0.5)
Decimal('0.5')
>>> Decimal(0.4)
Decimal('0.40000000000000002220446049250313080847263336181640625')

Note, the string python prints when you print a number is even more of an approximation, although Python uses an algorithm which should produce the shortest representation that can reliably reproduce the actual floating point value from the literal:

>>> 0.5 - 0.4
0.09999999999999998
>>> Decimal(0.09999999999999998)
Decimal('0.09999999999999997779553950749686919152736663818359375')
>>> Decimal(0.5 - 0.4)
Decimal('0.09999999999999997779553950749686919152736663818359375')

Note, pretty much any language I can think of uses a truncated form of the floating point number that is actually represented in hardware. If you want to see more, you generally have to use string formatting.

juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
  • Is your answer related to why print(0.1) is 0.1 not 0.1000000000000xxx? If yes, how they related with? Thanks. – sam Aug 01 '22 at 22:59
  • @sam please re-read – juanpa.arrivillaga Aug 01 '22 at 23:03
  • @sam the representation you see from `print(x)` does not reflect the floating point value – juanpa.arrivillaga Aug 01 '22 at 23:04
  • I think your answer precisely described 0.5-0.4 is not equal to 0.4-0.3, 0.3-0.2, because 0.4, 0.3 numbers also have an error,right? However, I think print(0.1) should print 0.1000000000xxx or 0.999999999xxx, why print(0.1)'s 0.1 should not be effect with hardware? – sam Aug 01 '22 at 23:07
  • 1
    @sam **because `print` is lying to you** for the sake of aesthetics – juanpa.arrivillaga Aug 01 '22 at 23:08
  • 2
    @sam the string representation that is printed is the *shortest one that reliably reproduces the actual float* (although that is a CPython implementation detail, as long as it is reliable, it doesn't have to be the shortest I don't think) – juanpa.arrivillaga Aug 01 '22 at 23:09
  • I do not accept python could do such "aesthetics", because 0.1 is calculated with 0.5-0.4,right? – sam Aug 01 '22 at 23:12
  • 1
    @sam **no** no it isn't. What do you not understand about `0.1 != 0.5 - 0.4`? – juanpa.arrivillaga Aug 01 '22 at 23:12
  • @sam again, all languages lie to you in this sense. – juanpa.arrivillaga Aug 01 '22 at 23:13
  • 3
    I'll just add that `0.1 == 0.2 - 0.1` is actually `True`. While neither `0.1` nor `0.2` can be represented exactly, `0.2` is represented exactly as 2 times as much as `0.1`. It's because these two numbers have the same mantissa and only differ by exponent. That's why this specific subtraction behaves differently than the other ones. – Piotr Siupa Aug 01 '22 at 23:33
  • 1
    Let me try to summary @juanpa.arrivillaga answer: Due to python's "aesthetics", python print(0.1)=0.1 only on the situation of visualization, but when calculation, python will let 0.1 saved as 0.1000000000000000055511151231257827021181583404541015625, and 0.4, 0.3 also have its own error, so the - operation will cause all the comparison to be not equal,right? – sam Aug 01 '22 at 23:33
  • 1
    I found that what I really confused is if python use use "aesthetics" to show 0.1, why python couldn't use "aesthetics" to show 0.4-0.3 also 0.1? – sam Aug 02 '22 at 00:16
  • @sam I've explained it multiple times, because it uses *the shortest representation that will reliably reproduce the actual value* – juanpa.arrivillaga Aug 02 '22 at 01:18
  • 2
    @sam: It's partly a consequence of two design decisions in Python 3: first, that the `repr` of a float should be "faithful" (that is, different floats should have different `repr`s), and second, that the `str` and `repr` of a `float` should be identical. It follows from those choices that if two (non-NaN) floats do not compare equal then they'll `print` differently. Note that Python 2 made different choices, and in Python 2, `print(0.5 - 0.4)` _will_ give the same output as `print(0.1)`, despite that the two values being printed are not equal to one another. – Mark Dickinson Aug 02 '22 at 10:33
  • 1
    `repr` being faithful is important because it's a precondition for being able to reconstruct a float from its `repr` (e.g., via `eval` or the `float` constructor). – Mark Dickinson Aug 02 '22 at 10:34
  • That would be interesting to understand what's exactly behind the `Decimal` module because 0.1 in the IEEE double-precision format should be I believe Sum[2^(-4*i) + 2^(-4*i - 1), {i, 1, 12}] which is approximated as 0.09999999999999964472863211994990706 with 35 digits of accuracy. This number disagress with the one indicated above. I am not sure why. – pluton Jan 24 '23 at 15:53
  • @pluton I don't think so. It's just the same number I would get if I create a little C program to do : `printf("%.55f\n", (double) 0.1);` which gives me `0.1000000000000000055511151231257827021181583404541015625` – juanpa.arrivillaga Jan 24 '23 at 18:50
  • @juanpa.arrivillaga If I apply the IEEE double-precision binary format as I know it (the basic definition), I get (0.1)decimal = (1.1001100110011001100110011001100110011001100110011010)binary * 2^(-4)) where I use rounding up on the last entry 53) and the number is 3602879701896397/36028797018963968 which can be approximated as 0.10000000000000000555111512312578270 which is the one you show. Good! There was an issue in my initial attempt. – pluton Jan 24 '23 at 19:13