1

Parts of this question have been addressed elsewhere (e.g. is floating point math broken?).

The following reveals a difference in the way numbers are generated by division vs multiplication:

>>> listd = [i/10 for i in range(6)]
>>> listm = [i*0.1 for i in range(6)]
>>> print(listd)
[0.0, 0.1, 0.2, 0.3, 0.4, 0.5]
>>> print(listm)
[0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5]

In the second case, 0.3 has a rounding error of about 1e-16, double floating point precision.

But I don't understand three things about the output:

  1. Since the only numbers here exactly representable in binary are 0.0 and 0.5, why aren't those the only exact numbers printed above?
  2. Why do the two list comprehensions evaluate differently?
  3. Why are the two string representations of the numbers different, but not their binary representations?
>>> def bf(x):
        return bin(struct.unpack('@i',struct.pack('!f',float(x)))[0])
>>> x1 = 3/10
>>> x2 = 3*0.1
>>> print(repr(x1).ljust(20), "=", bf(x1))
>>> print(repr(x2).ljust(20), "=", bf(x2))
0.3                  = -0b1100101011001100110011011000010
0.30000000000000004  = -0b1100101011001100110011011000010
mbdeaton
  • 54
  • 6

2 Answers2

3

The critical point here is that 10 is represented exactly in binary, whereas 0.1 is not. Dividing by 10 gets you the closest possible representation for each fraction; multiplying by the inexact conversion of 0.1 does not guarantee precision. Sometimes you get "close enough" to round off the result to a single decimal place, sometimes not.

Is that enough rationale?

Prune
  • 76,765
  • 14
  • 60
  • 81
  • 1
    The typical rule in printing floating point numbers is that you print out enough decimal places so that if the user actually typed in what was printed out, they'd get the exact value being printed. 0.4 isn't an exact binary value, but the result generated by 4/10 gives the identical floating point number that 0.4 does, so that's all that needs to be printed. – Frank Yellin Oct 29 '20 at 00:45
  • "round off the result to a single decimal place"...is that something the Python shell does automatically? – mbdeaton Oct 29 '20 at 00:46
  • 2
    @mbdeaton: Not the shell, but the `repr` of `float`s does that for you. It only rounds as much as it can without affecting reproducibility; typing `0.10000000000000000555` gets the same value as typing `0.1`, so it will represent both as `0.1`. By contrast, `0.3` is equivalent to `0.2999999999999999889`, not `0.30000000000000004`, so `0.30000000000000004` can't be shortened to `0.3`. – ShadowRanger Oct 29 '20 at 00:51
3

Answering each question:

  1. Since the only numbers here exactly representable in binary are 0.0 and 0.5, why aren't those the only exact numbers printed above?

Python rounds off the display of any floating point number to the shortest literal that produces the same value when evaluated. So yes, many of those numbers aren't actually the same as the actual number they represent, but if you typed them in in Python, you'd get that (slightly inaccurate) value without the math.

  1. Why do the two list comprehensions evaluate differently?

0.1 is already inaccurate, as you've stated, so multiplying by it is not exactly equivalent to dividing by 10 (where at least both inputs are precise integers). Sometimes that inaccuracy means the result is not the same as dividing by 10; after all, you multiplied by "just over one tenth", not "one tenth".

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271