15. 浮点运算:问题和限制 — Python 文档

来自菜鸟教程
Python/docs/3.9/tutorial/floatingpoint
跳转至:导航、​搜索

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 这样的数字可以精确表示)。

如果您是浮点运算的重度用户,您应该查看 SciPy 项目提供的 NumPy 包和许多其他用于数学和统计运算的包。 见 < 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”示例,并展示了如何自己对此类案例进行精确分析。 假设基本熟悉二进制浮点表示。

表示错误 指的是某些(实际上,大多数)十进制小数不能完全表示为二进制(基数 2)小数。 这是 Python(或 Perl、C、C++、Java、Fortran 等)通常不会显示您期望的确切十进制数的主要原因。

这是为什么? 1/10 不能完全表示为二进制分数。 今天(2000 年 11 月)几乎所有机器都使用 IEEE-754 浮点运算,几乎所有平台都将 Python 浮点数映射到 IEEE-754 “双精度”。 754 个双精度包含 53 位精度,因此在输入时,计算机力求将 0.1 转换为最接近的分数,其形式为 J/2**N 其中 J[ X169X] 是一个正好包含 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'