15. 浮点运算︰ 问题和限制

浮点数在计算机硬件中表示为以 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...

在任何有限数量的位停下来,你得到的都是近似值。今天在大多数机器上,浮点数的近似使用的小数以最高的53位为分子,2的幂为分母。对于1/10这种情况,二进制分数是3602879701896397 / 2 ** 55,它接近但不完全等于真值1/10。

由于显示方式的原因,许多使用者意识不到是近似值。Python 只打印机器中存储的二进制值的十进制近似值。在大多数机器上,如果 Python 要打印 0.1 存储的二进制的真正近似值,将会显示

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

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

>>> 1 / 10
0.1

需要记住的是即使打印的结果看上去是精确的1/10,真正存储的值是最近似的二进制小数。

有趣的是,有许多不同的十进制数使用相同的最接近的二进制小数。例如,数字0.10.100000000000000010.1000000000000000055511151231257827021181583404541015625都由3602879701896397 / 2 ** 55近似。由于所有这些十进制值具有相同的近似值,因此可以显示它们中的任何一个,同时仍然保持不变量eval(repr(x)) == x

在之前的版本,Python提示符和内置的repr()函数将显示一个具有17位有效数字的小数,0.10000000000000001从Python 3.1开始,Python(在大多数系统上)现在能够选择其中最短的,只显示0.1

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

为了更好看的输出,你可能想用字符串格式化来生成固定位数的有效数字:

>>> format(math.pi, '.12g')  # give 12 significant digits
'3.14159265359'

>>> format(math.pi, '.2f')   # give 2 digits after the point
'3.14'

>>> repr(math.pi)
'3.141592653589793'

重要的是要意识到,在实际意义上,这是一个错觉:你只是舍入真实机器值的显示

一个错觉可能产生另一个错觉。例如,由于0.1不完全是1/10,所以对三个值0.1的求和不能精确地产生0.3:

>>> .1 + .1 + .1 == .3
False

另外,由于0.1不能接近1/10的精确值,0.3不能接近3/10的精确值,那么用round()函数进行预舍入无法帮助:

>>> round(.1, 1) + round(.1, 1) + round(.1, 1) == round(.3, 1)
False

虽然数字无法更接近其预期的精确值,但是round()函数可用于后舍入,以使具有不精确值的结果变得可以相互比较:

>>> round(.1 + .1 + .1, 10) == round(.3, 10)
True

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

正如接近结束所说的,“没有简单的答案”。仍然,不要对浮点数过分谨慎!Python 浮点数计算中的误差源之于浮点数硬件,大多数机器上每次计算误差不超过 2**53 分之一。这对于大多数任务来说是足够的,但是你需要记住,它不是十进制算术,并且每个浮点操作都可能遭受新的舍入误差。

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

对于需要精确十进制表示的情形,尝试使用decimal模块,该模块实现适用于会计应用程序和高精度应用程序的十进制算术。

精确算术的另一种形式由fractions模块支持,其实现基于有理数的算术(因此,可以精确表示如1/3的数字)。

如果你是浮点数操作的重度使用者,你应该看一下由SciPy项目提供的Numerical Python包和其它用于数学和统计学的包。参见<https://scipy.org>。

当你真的想要知道浮点数精确值的时候,Python 提供这样的工具可以帮助你。float.as_integer_ratio()方法将float的值表达为分数:

>>> x = 3.14159
>>> x.as_integer_ratio()
(3537115888337719, 1125899906842624)

因为比值是精确的,它可以用来无损地重新生成初始值:

>>> x == 3537115888337719 / 1125899906842624
True

float.hex()方法表示十六进制(基数16)的浮点数,再次给出计算机存储的精确值:

>>> x.hex()
'0x1.921f9f01b866ep+1'

精确的十六进制表示可以用来准确地重新构建浮点数:

>>> x == float.fromhex('0x1.921f9f01b866ep+1')
True

因为可以精确表示,所以可以用在不同版本的Python(与平台相关)之间可靠地移植数据以及与支持同样格式的其它语言(例如Java和C99)交换数据。

另一个有用的工具是math.fsum()函数,它有助于减少求和期间的精度损失。它跟踪“丢失的数字”,并将值添加到总计之上。这可以给总体的准确度带来不同,以至于错误不会累积到影响最终结果的点:

>>> sum([0.1] * 10) == 1.0
False
>>> math.fsum([0.1] * 10) == 1.0
True

15.1. 二进制表示的误差

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

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

这是为什么?1/10不能精确地表示为二进制分数。今天几乎所有的机器(2000年11月)使用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位(>= 2**52但是< 2**53),所以 N 的最佳值是56:

>>> 2**52 <=  2**56 // 10  < 2**53
True

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

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

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

>>> q+1
7205759403792794

因此在 754 双精度下 1/10 的最佳近似是:

7205759403792794 / 2 ** 56

分子和分母都除以2将小数缩小到:

3602879701896397 / 2 ** 55

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

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

>>> 0.1 * 2 ** 55
3602879701896397.0

如果我们将这个分数乘以10**55,我们可以看到值到55个十进制数字:

>>> 3602879701896397 * 10 ** 55 // 2 ** 55
1000000000000000055511151231257827021181583404541015625

这意味着计算机中存储的确切数字等于十进制值0.1000000000000000055511151231257827021181583404541015625。许多语言(包括旧版本的Python)会把结果舍入到17位有效数字,而不是显示全部的十进制值:

>>> format(0.1, '.17f')
'0.10000000000000001'

fractionsdecimal模块使这些计算变得容易:

>>> from decimal import Decimal
>>> from fractions import Fraction

>>> Fraction.from_float(0.1)
Fraction(3602879701896397, 36028797018963968)

>>> (0.1).as_integer_ratio()
(3602879701896397, 36028797018963968)

>>> Decimal.from_float(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')

>>> format(Decimal.from_float(0.1), '.17')
'0.10000000000000001'