14. 浮点数运算:问题和局限

优质
小牛编辑
132浏览
2023-12-01

14. 浮点数运算:问题和局限

浮点数在计算机硬件中表示为以 2 为底(二进制)的小数。例如,十进制小数

0.125

是1/10 + 2/100 + 5/1000 的值,同样二进制小数

0.001

是 0/2 + 0/4 + 1/8 的值。这两个小数具有相同的值,唯一真正的区别是,第一个小数是十进制表示法,第二个是二进制表示法。

不幸的是,大多数十进制小数不能完全用二进制小数表示。结果是,一般情况下,你输入的十进制浮点数仅由实际存储在计算机中的近似的二进制浮点数表示。

这个问题在十进制情况下很容易理解。考虑分数 1/3,你可以用十进制小数近似它:

0.3

或者更接近的

0.33

或者再接近一点的

0.333

等等。无论你愿意写多少位数字,结果永远不会是精确的 1/3,但将会越来越好地逼近 1/3。

同样地,无论你使用多少位的二进制数,都无法确切地表示十进制值 0.1。1/10 用二进制表示是一个无限循环的小数。

0.0001100110011001100110011001100110011001100110011...

在任何有限数量的位停下来,你得到的都是近似值。

在一台运行 Python 的典型机器上, Python 浮点数具有 53 位的精度,所以你输入的十进制数0.1存储在内部的是二进制小数

0.00011001100110011001100110011001100110011001100110011010

非常接近,但不完全等于 1/10。

由于解释器显示浮点数的方式,很容易忘记存储在计算机中的值是原始的十进制小数的近似。Python 只打印机器中存储的二进制值的十进制近似值。如果 Python 要打印 0.1 存储的二进制的真正近似值,将会显示

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

这么多位的数字对大多数人是没有用的,所以 Python 显示一个舍入的值

>>> 0.1
0.1

意识到在真正意义上这是一种错觉是很重要的:机器中的值不是精确的 1/10,它显示 的只是机器中真实值的舍入。一旦你用下面的数值进行算术运算,这个事实就变得很明显了

>>> 0.1 + 0.2
0.30000000000000004

注意,这是二进制浮点数的自然性质:它不是 Python 中的一个 bug ,也不是你的代码中的 bug。你会看到所有支持硬件浮点数算法的语言都会有这个现象(尽管有些语言默认情况下或者在所有输出模式下可能不会显示 出差异)。

还有其它意想不到的。例如,如果你舍入2.675到两位小数,你得到的是

>>> round(2.675, 2)
2.67

内置round()函数的文档说它舍入到最接近的值,rounding ties away from zero。因为小数 2.675 正好是 2.67 和 2.68 的中间,你可能期望这里的结果是(二进制近似为) 2.68。但是不是的,因为当十进制字符串2.675转换为一个二进制浮点数时,它仍然被替换为一个二进制的近似值,其确切的值是

2.67499999999999982236431605997495353221893310546875

因为这个近似值稍微接近 2.67 而不是 2.68,所以向下舍入。

如果你的情况需要考虑十进制的中位数是如何被舍入的,你应该考虑使用decimal模块。顺便说一下,decimal模块还提供了很好的方式可以“看到”任何 Python 浮点数的精确值。

>>> from decimal import Decimal
>>> Decimal(2.675)
Decimal('2.67499999999999982236431605997495353221893310546875')

另一个结果是,因为 0.1 不是精确的 1/10,十个值为 0.1 数相加可能也不会正好是 1.0:

>>> sum = 0.0
>>> for i in range(10):
...     sum += 0.1
...
>>> sum
0.9999999999999999

二进制浮点数计算有很多这样意想不到的结果。“0.1”的问题在下面"误差的表示"一节中有准确详细的解释。更完整的常见怪异现象请参见浮点数的危险

最后我要说,“没有简单的答案”。也不要过分小心浮点数!Python 浮点数计算中的误差源之于浮点数硬件,大多数机器上每次计算误差不超过 2**53 分之一。对于大多数任务这已经足够了,但是你要在心中记住这不是十进制算法,每个浮点数计算可能会带来一个新的舍入错误。

虽然确实有问题存在,对于大多数平常的浮点数运算,你只要简单地将最终显示的结果舍入到你期望的十进制位数,你就会得到你期望的最终结果。关于如何精确控制浮点数的显示请参阅格式化字符串的语法str.format()方法的格式说明符。

14.1. 二进制表示的误差

这一节将详细解释“0.1”那个示例,并向你演示对于类似的情况自已如何做一个精确的分析。假设你已经基本了解浮点数的二进制表示。

二进制表示的误差 指的是这一事实,一些(实际上是大多数) 十进制小数不能精确地用二进制小数表示。这是为什么 Python(或者 Perl、 C、 C++、 Java、 Fortran和其他许多语言)通常不会显示你期望的精确的十进制数的主要原因:

>>> 0.1 + 0.2
0.30000000000000004

这是为什么?1/10 和 2/10 不能用二进制小数精确表示。今天(2010 年 7 月)几乎所有的机器都使用 IEEE-754 浮点数算法,几乎所有的平台都将 Python 的浮点数映射成 IEEE-754“双精度浮点数”。754 双精度浮点数包含 53 位的精度,所以输入时计算机努力将 0.1 转换为最接近的 J/2**N 形式的小数,其中J 是一个 53 位的整数。改写

1 / 10 ~= J / (2**N)

J ~= 2**N / 10

回想一下 J 有 53 位(>=252但<253),所以 N 的最佳值是 56:

>>> 2**52
4503599627370496
>>> 2**53
9007199254740992
>>> 2**56/10
7205759403792793

即 56 是 N 保证 J 具有 53 位精度的唯一可能的值。J 可能的最佳值是商的舍入:

>>> q, r = divmod(2**56, 10)
>>> r
6

由于余数大于 10 的一半,最佳的近似值是向上舍入:

>>> q+1
7205759403792794

因此在 754 双精度下 1/10 的最佳近似是 J 取大于 2**56 的那个数,即

7205759403792794 / 72057594037927936

请注意由于我们向上舍入,这其实有点大于 1/10;如果我们没有向上舍入,商数就会有点小于 1/10。但在任何情况下它都不可能是精确的 1/10!

所以计算机从来没有"看到"1/10: 它看到的是上面给出的精确的小数,754 双精度下可以获得的最佳的近似了:

>>> .1 * 2**56
7205759403792794.0

如果我们把这小数乘以 10**30,我们可以看到其(截断后的)值的最大 30 位的十进制数:

>>> 7205759403792794 * 10**30 // 2**56
100000000000000005551115123125L

也就是说存储在计算机中的精确数字约等于十进制值 0.100000000000000005551115123125。以前 Python 2.7 和 Python 3.1 版本中,Python 四舍五入到 17 个有效位,给出的值是 '0.10000000000000001'。在当前版本中,Python 显示一个最短的十进制小数,它会正确舍入真实的二进制值,结果就是简单的‘0.1’。