Skip to main content

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中的错误,它不是代码中的错误。你会看到支持你的硬件浮点运算的所有语言中的相同类型的东西(虽然一些语言可能不是 display 默认情况下的差别,或在所有输出模式)。

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

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

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

一个错觉可能是另一个错觉。例如,由于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提供了工具,可以帮助在那些罕见的场合,当你真的 do 想知道一个浮点数的确切值。 float.as_integer_ratio() 方法将浮点的值表示为分数:

>>> 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”示例,并说明如何自行执行此类案例的精确分析。假定基本熟悉二进制浮点表示。

Representation error 指的是一些(大多数,实际上)十进制小数不能精确地表示为二进制(基本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 的唯一值,其使得 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小一点。但在任何情况下都不能是 exactly 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'