0

There's something wrong with my understanding of either the round function in python or how doubles are represented in python. I find a very perplexing behavior when rounding numbers that have a double precision rounding error; for example 1.01046+.00002=1.0104799999999998. When I round this to 5 digits, python reports the result as 1.01048, the expected result. But I thought the whole issue was that 1.01046+.00002=1.0104799999999998 because there's no double value that exactly equals 1.01048. If round takes in a double and returns a double, how can it return 1.01048 if no such double exists?

The following code: print([1.01046+.00002*i for i in range(6)])

outputs: [1.01046, 1.0104799999999998, 1.0105, 1.0105199999999999, 1.01054, 1.01056]

wheras this code: print([round(1.01046+.00002*i,5) for i in range(6)])

outputs: [1.01046, 1.01048, 1.0105, 1.01052, 1.01054, 1.01056]

why is it that python doesn't have an exact value for 1.01046+.00002*1, python says "I don't have any such number; I can give you a number that's really close though", but when I ask python to round the resulting number, it goes "oh yeah of course that's 1.01048"

2 Answers 2

3

The bit you're missing is that str(float) and repr(float) return the shortest string that reproduces the original value when passed to float().

So, e.g., despite that there is no representable float exactly equal to one tenth,

>>> 0.1
0.1

Doesn't mean it is one tenth (and it isn't!), but that float("0.1") will return the original value denoted by the literal "0.1", but no shorter string will.

In your example,

>>> import math
>>> 1.01046 + .00002
1.0104799999999998 # shortest string that converts back exactly

But there's a different float closer to a multiple of (the mathematical) 1e-5, which is what passing 5 as the second argument to round() is asking for. It happens to be exactly 1 unit in the last place larger than the float we computed above:

>>> math.nextafter(_, math.inf) # one ULP larger
1.01048

But again, that's not 1.01048 exactly, but the shortest string that coverts back to the true value:

>>> import decimal
>>> decimal.Decimal(_)
Decimal('1.0104800000000000448352466264623217284679412841796875')

So the true value returned by round() is a smidgen larger than 1.01048.

Under the covers

round() is not a simple function. In CPython, it works hard to deliver the best possible result. This can require arbitrary precision arithmetic, to do correctly rounded binary <-> decimal conversions.

In your case, the result of 1.01046 + .00002 is first converted to a correctly rounded decimal string with 5 digits after the decimal point (not a Python-level string, but a C-level buffer of characters). That string is "1.01048". Then that string is converted back to a correctly rounded binary float.

Note that it's no accident that the result displays as 1.01048 too: since it came from converting the string "1.01048" to a float, and str/repr() return the shortest string that reproduces their input, str/repr() cannot return a string longer than "1.01048".

But, as already shown, the actual float value is actually a bit larger than that.

Sign up to request clarification or add additional context in comments.

1 Comment

Note: I added info in a new section at the bottom.
1

The result of rounding isn't exact, either. But when numbers are printed, there's a default number of significant digits, and it's close enough that it gets printed without any trailing errors.

You can see this if you force the output to 20 digits after the decimal point.

>>> for i in range(6):
...     val = 1.01046+.00002*i
...     print(val, round(val, 5), f'{val:.20f} {round(val, 5):.20f}')
... 
1.01046 1.01046 1.01045999999999991381 1.01045999999999991381
1.0104799999999998 1.01048 1.01047999999999982279 1.01048000000000004484
1.0105 1.0105 1.01049999999999995381 1.01049999999999995381
1.0105199999999999 1.01052 1.01051999999999986279 1.01052000000000008484
1.01054 1.01054 1.01053999999999999382 1.01053999999999999382
1.01056 1.01056 1.01055999999999990280 1.01055999999999990280

In the cases where val and round(val, 5) are different, the difference is after the 16th digit after the decimal point, and that's not shown by default.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.