为什么Python的默认round函数修复了双重舍入误差?
我对Python中的round函数或者双精度数的表示方式有些困惑。在处理一些有双精度舍入误差的数字时,我发现了一个很奇怪的现象。例如,1.01046加上0.00002的结果是1.0104799999999998。当我把这个数字四舍五入到5位小数时,Python给出的结果是1.01048,这个结果是我预期的。但是我以为问题在于1.01046加上0.00002的结果是1.0104799999999998,因为没有一个双精度数能准确等于1.01048。如果round函数接受一个双精度数并返回一个双精度数,那它怎么能返回1.01048呢?因为根本没有这样一个双精度数。
下面这段代码:
print([1.01046+.00002*i for i in range(6)])
输出是:
[1.01046, 1.0104799999999998, 1.0105, 1.0105199999999999, 1.01054, 1.01056]
而这段代码:
print([round(1.01046+.00002*i,5) for i in range(6)])
输出是:
[1.01046, 1.01048, 1.0105, 1.01052, 1.01054, 1.01056]
为什么Python没有一个精确的值来表示1.01046加上0.00002乘以1呢?Python说“我没有这样的数字;不过我可以给你一个非常接近的数字”,但是当我让Python对这个结果进行四舍五入时,它却说“哦,没问题,这就是1.01048”。
2 个回答
四舍五入的结果也不是完全准确的。不过,当数字被打印出来时,默认情况下会显示一定数量的有效数字,这个数量足够接近,所以打印出来的时候不会出现多余的错误。
如果你强制输出小数点后20位数字,就能看到这一点。
>>> 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
在val
和round(val, 5)
不一样的情况下,它们的差别出现在小数点后第16位之后,而默认情况下是看不到这些的。
你缺少的一个点是,str(float)
和 repr(float)
返回的是最短的字符串,这个字符串在传给 float()
时能还原成原来的值。
举个例子,尽管没有一个浮点数能准确表示十分之一,
>>> 0.1
0.1
这并不意味着它就是十分之一(其实不是!),而是 float("0.1")
会返回由字面量 "0.1"
表示的原始值,但没有更短的字符串能做到这一点。
在你的例子中,
>>> import math
>>> 1.01046 + .00002
1.0104799999999998 # shortest string that converts back exactly
但有一个浮点数更接近于(数学上的)1e-5的倍数,这正是将5作为 round()
的第二个参数所要求的。这个浮点数恰好比我们上面计算的浮点数大1个单位:
>>> math.nextafter(_, math.inf) # one ULP larger
1.01048
但再次强调,这并不是准确的1.01048,而是能转换回真实值的最短字符串:
>>> import decimal
>>> decimal.Decimal(_)
Decimal('1.0104800000000000448352466264623217284679412841796875')
所以 round()
返回的真实值比1.01048稍微大一点。
深入了解
round()
不是一个简单的函数。在 CPython 中,它努力提供最佳的结果。这可能需要任意精度的算术运算,以正确地进行二进制和十进制之间的转换。
在你的例子中,1.01046 + .00002
的结果首先被转换为一个正确四舍五入的十进制字符串,保留小数点后5位(这不是Python层面的字符串,而是C层面的字符缓冲区)。这个字符串是 "1.01048"
。然后这个字符串又被转换回一个正确四舍五入的二进制浮点数。
注意,结果显示为1.01048并不是偶然:因为它是通过将字符串 "1.01048" 转换为浮点数得到的,而 str/repr()
返回的是能还原输入的最短字符串,所以 str/repr()
不能返回比 "1.01048" 更长的字符串。
但是,正如之前所示,实际的浮点值实际上比这个值稍微大一点。