15. 浮点数算法:争议和限制
浮点数在计算机硬件中以二进制分数表示。举例而言,十进制分数:
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.1
和 0.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'
使用 fractions
和 decimal
模块可以简化这些计算:
>>> 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'
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。