15. 浮点算法:问题和局限性

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

0.125

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

0.001

值是 0/2 0/4 1/8.这两个分数具有相同的值,唯一的实际区别是,第一个分数以 10 为基数的分数表示,第二个分数以 2 为基数。

不幸的是,大多数十进制分数不能完全表示为二进制分数。结果是,通常,您 Importing 的十进制浮点数仅由计算机中实际存储的二进制浮点数近似。

首先从 10 开始更容易理解该问题。考虑分数 1/3.您可以将其近似为以 10 为基数的分数:

0.3

or, better,

0.33

or, better,

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 pass显示舍入的值来保持数字的可 Management 性。

>>> 1 / 10
0.1

请记住,即使打印结果看起来像精确的 1/10 值,实际的存储值也是最接近的可表示二进制分数。

有趣的是,有许多不同的十进制数共享相同的最接近的近似二进制分数。例如,数字0.10.10000000000000001以及0.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 个部分。对于大多数任务来说,这已经足够了,但是您需要记住,它不是十进制算术,并且每个 float 操作都可能遇到新的舍入错误。

尽管确实存在病理情况,但是如果将finally结果的显示四舍五入为期望的小数位数,那么对于大多数随意使用浮点算术的情况,finally都会看到期望的结果。 str()通常就足够了,有关更好的控制,请参见格式字符串语法中的str.format()方法的格式说明符。

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

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

如果您是浮点运算的重度用户,则应查看 SciPy 项目提供的 Numeric Python 软件包以及许多其他用于 math 和统计运算的软件包。参见< 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'

此精确的十六进制表示形式可用于精确地重构 float 值:

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

由于表示形式是精确的,因此对于可靠地跨不同版本的 Python 移植值(平台独立性)以及与支持相同格式的其他语言(例如 Java 和 C99)交换数据非常有用。

另一个有用的工具是math.fsum()函数,该函数可帮助减轻求和过程中的精度损失。当值添加到运行总计时,它会跟踪“丢失的数字”。这可能会影响整体准确性,因此误差不会累积到影响finally总数的程度:

>>> 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 浮点数 Map 到 IEEE-754“双精度”。 754 个双精度数包含 53 位精度,因此在 Importing 时,计算机努力将 0.1 转换为* J */2 ** * N 形式的最接近分数,其中 J *是正好包含 53 位的整数。Rewrite

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

as

J ~= 2**N / 10

并回想起* J 正好有 53 位(是>= 2**52< 2**53), N *的最佳值为 56:

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

也就是说,* N 的唯一值是正好有 53 位的 N 。那么, J *的最佳可能值就是四舍五入的商:

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

由于余数大于 10 的一半,因此pass四舍五入获得最佳近似值:

>>> q+1
7205759403792794

因此,在 754 倍精度中,最可能近似为 1/10 的是:

7205759403792794 / 2 ** 56

将分子和分母都除以二可将分数减小为:

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'