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

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

0.125

具有值1/10 + 2/100 + 5/1000,并且以相同的方式具有二进制分数

0.001

具有值0/2 + 0/4 + 1/8。这两个分数具有相同的值,唯一的真正的区别是第一个以基本10分数符号写,第二个在基数2。

不幸的是,大多数小数分数不能精确地表示为二进制分数。结果是,一般来说,输入的十进制浮点数只是由实际存储在机器中的二进制浮点数近似。

这个问题在10号基础上更容易理解。考虑1/3的分数。您可以近似为10分数:

0.3

或者,更好,

0.33

或者,更好,

0.333

等等。无论你愿意写多少个数字,结果永远不会正好是1/3,但将是一个越来越好的近似1/3。

同样,无论您愿意使用多少基数2,十进制值0.1都不能精确地表示为基数2分数。在碱基2中,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中的错误,它不是代码中的错误。您将在所有语言中看到支持硬件浮点运算的相同类型的东西(尽管一些语言可能不会默认显示差异,或在所有输出模式下)。

为了更愉悦的输出,您可能希望使用字符串格式化以产生有限数目的有效数字:

>>> 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中不超过1个部分。这对于大多数任务来说是足够的,但是你需要记住,它不是十进制算术,并且每个浮点操作都可能遭受新的舍入误差。

虽然病理情况确实存在,但对于大多数偶然使用浮点运算,如果只是将最终结果的显示轮转到期望的小数位数,则会看到结果。str()通常就足够了,对于更精细的控制,请参阅格式字符串语法str.format()方法的格式化指令。

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

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

如果你是一个沉重的浮点操作的用户,你应该看看数字Python包和许多其他包由SciPy项目提供的数学和统计操作。参见<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”示例,并说明如何自行执行此类案例的精确分析。假定基本熟悉二进制浮点表示。

表示错误指的是一些(大多数,实际上)十进制小数不能精确地表示为二进制(基本2)小数的事实。这是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的唯一值,其使得具有恰好53位的JJ的最佳可能值是商舍入:

>>> 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'