15. 浮点数算法:争议和限制

未匹配的标注
本文档最新版为 3.8,旧版本可能放弃维护,推荐阅读最新版!

浮点数在计算机硬件中以二进制分数表示。举例而言,十进制分数:

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

在任何一个位置停下,你都只能得到一个近似值。因此,在今天的大部分架构上,浮点数都只能近似地使用二进制分数表达,分子使用每 8 字节的前 53 位表示,分母则表示为 2 的幂次。在 1/10 这个例子中,相应的二进制分数是 3602879701896397 / 2 ** 55 ,它很接近 1/10 ,但并不是 1/10 。

大部分用户都不会意识到这个差异的存在,因为 Python 只会打印计算机中存储的二进制值的十进制近似值。在大部分计算机中,如果 Python 想把 0.1 的二进制对应的精确十进制打印出来,将会变成这样:

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

对于大部分人来说后面这么多的近乎乱码的数字并没什么用,所以 Python 会舍入到可控制的精度。

>>> 1 / 10
0.1

牢记,即使输出的结果看起来好像就是 1/10 的精确值,实际储存的值只是最接近 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 的 bug ,也不是你代码编辑器的 bug 。你可以在所有支持你硬件浮点计算的编程语言中注意到同样的情况(尽管某些其它语言可能默认或者根本无法输出精度差)。

为了更加灵活地输出浮点数,你可能会需要字符串格式化函数来输出特定有效数字的浮点数表达。

>>> format(math.pi, '.12g')  # 保留 12 位有效精度
'3.14159265359'

>>> format(math.pi, '.2f')   # 保留小数点后 2 位
'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” 的问题,将在下面的 “表示错误” 章节继续详细地解释。你可以参考 The Perils of Floating Point (浮点的风险... 发现更多常见的存在于浮点数计算中的“惊喜”。
尽管如链接指向的文章文末所说的,“(处理浮点数运算的危险)并没有一个简单的方法”。不过,也无须过度担心浮点数带来的问题! Python 的浮点数运算继承自浮点数处理硬件,而在大部分机器中,每次运算造成的误差都不会超过 1 / 2 ** 55

这对于大部分任务来说已经完全足够了,不过你还是要时刻牢记,这终究不是十进制运算,而且每次浮点运算都会造成新的舍入误差。

尽管某些极端情况存在,你会发现在大部分使用浮点数的情况下,你只需要在计算完成后对结果进行一次舍入就可以得到你期望的值。通常来说, str() 函数就够用了,而如果需要更灵活地控制精度,可以参考 格式化字符串 ,亦即 str.format() 方法。

对于需要精确的十进制表示的用例,请尝试使用 decimal 模块,它实现了适用于会计应用程序和高精度应用程序的十进制浮点算术。

fractions 模块支持另一种精确的非整数算术,它使用分数实现了有理数之间的各种运算(因此像 1/3 这样的数就可以被精确地表示)。

如果你有大量的浮点运算需求,你应当考虑使用 Numerical Python 包和其它许多由 SciPy 项目提供的、为数学和统计学运算设计的包。参见 <scipy.org >。

在某些罕见情况下,你可能需要知道某个浮点数的具体值, Python 也为此提供了一系列工具。

float.as_integer_ratio() 方法可以将一个浮点数值表示为二进制分数。

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

因为这个分数是准确的,它可以被用来无损地重新构建原来的浮点数:

>>> x == 3537115888337719 / 1125899906842624
True

float.hex() 方法可以将一个浮点数以十六进制的形式表示出来,以另一种形式返回你的计算机存储的浮点数的确切值:

>>> 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 的 float 类型表示为 IEEE-754 中的 “double 精度” 浮点数。 IEEE-754 double 包含 53 比特的精度,于是在计算机读入浮点数 0.1 后,会尽可能地将 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 是唯一一个能使得 J 恰好为 53 比特的 N 值。那么 J 最佳的可能值就是舍入后的商:

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

因为余数比 10 的一半要大,所以商需要向上进位:

>>> q+1
7205759403792794

所以在 IEEE-754 标准 double 精度中最接近 1/10 的值就是:

7205759403792794 / 2 ** 56

约分得:

3602879701896397 / 2 ** 55

注意到我们向上进位了,因此这个值会比 1/10 稍微大一点;如果我们没有进位,则比 1/10 稍微小一点点。但是永远也不可能恰好等于 1/10 。

如此一般,我们在计算机中永远“看不到” 1/10 :它实际上只能是上面给出的具体分数、在 IEEE-754 的 double 精度中能给出的最近似值:

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

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/python-tutorial...

译文地址:https://learnku.com/docs/python-tutorial...

上一篇 下一篇
贡献者:1
讨论数量: 0
发起讨论 只看当前版本


暂无话题~