为什么Python的默认round函数修复了双重舍入误差?

0 投票
2 回答
39 浏览
提问于 2025-04-12 08:37

我对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 个回答

1

四舍五入的结果也不是完全准确的。不过,当数字被打印出来时,默认情况下会显示一定数量的有效数字,这个数量足够接近,所以打印出来的时候不会出现多余的错误。

如果你强制输出小数点后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

valround(val, 5)不一样的情况下,它们的差别出现在小数点后第16位之后,而默认情况下是看不到这些的。

3

你缺少的一个点是,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" 更长的字符串。

但是,正如之前所示,实际的浮点值实际上比这个值稍微大一点。

撰写回答