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

只要您停在任何有限的位數,您就只會得到近似值。而現在大多數的計算機中,浮點數是透過二進位分數近似的,其中分子從最高有效位元使開始用 53 個位元表示,分母則是以二為底的指數。在 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 的提示字元 (prompt) 與內建的 repr() 函式會選擇上段說明中有 17 個有效位元的數:0.10000000000000001。從 Python 3.1 版開始,Python(在大部分的系統上)可以選擇其中最短的數並簡單地顯示為 0.1

注意,這是二進位浮點數理所當然的特性,並不是 Python 的錯誤 (bug),更不是您程式碼的錯誤。只要有程式語言支持硬體的浮點數運算,您將會看到同樣的事情出現在其中(雖然某些程式語言預設不顯示差異,或者預設全部輸出)。

為求更優雅的輸出,您可能想要使用字串的格式化 (string formatting) 產生限定的有效位數:

>>> 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」的問題。如果想要其他常見驚喜更完整的描述,可以參考 The Perils of Floating Point(浮點數的風險)

正如那篇文章的结尾所言,“对此问题并无简单的答案。” 但是也不必过于担心浮点数的问题! Python 浮点运算中的错误是从浮点运算硬件继承而来,而在大多数机器上每次浮点运算得到的 2**53 数码位都会被作为 1 个整体来处理。 这对大多数任务来说都已足够,但你确实需要记住它并非十进制算术,且每次浮点运算都可能会导致新的舍入错误。

虽然病态的情况确实存在,但对于大多数正常的浮点运算使用来说,你只需简单地将最终显示的结果舍入为你期望的十进制数值即可得到你期望的结果。 str() 通常已足够,对于更精度的控制可参看 格式字符串语法str.format() 方法的格式描述符。

对于需要精确十进制表示的使用场景,请尝试使用 decimal 模块,该模块实现了适合会计应用和高精度应用的十进制运算。

另一种形式的精确运算由 fractions 模块提供支持,该模块实现了基于有理数的算术运算(因此可以精确表示像 1/3 这样的数值)。

如果你是浮点运算的重度用户,你应该看一下数值运算 Python 包 NumPy 以及由 SciPy 项目所提供的许多其它数学和统计运算包。 参见 <https://scipy.org>。

Python 也提供了一些工具,可以在你真的 想要 知道一个浮点数精确值的少数情况下提供帮助。 例如 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 为恰好包含 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

分子和分母都除以二则结果小数为:

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'