15. 浮點算術:問題與限制¶
浮點數在計算機硬體中表示為以 2 為基數(二進位制)的分數。例如,**十進位制**小數 0.625
的值為 6/10 + 2/100 + 5/1000,同樣地,**二進位制**小數 0.101
的值為 1/2 + 0/4 + 1/8。這兩個分數的值是相同的,唯一的實際區別是第一個以 10 為基數的小數表示法書寫,而第二個以 2 為基數。
不幸的是,大多數十進位制分數不能精確地表示為二進位制分數。結果是,通常情況下,您輸入的十進位制浮點數只能由機器中實際儲存的二進位制浮點數來近似表示。
這個問題首先以 10 為基數更容易理解。考慮分數 1/3。您可以將其近似為十進位制分數
0.3
或者,更好的是,
0.33
或者,更好的是,
0.333
等等。無論您願意寫多少位數字,結果都不會是精確的 1/3,但會是 1/3 的越來越好的近似值。
同樣地,無論您願意使用多少個二進位制位,十進位制值 0.1 都不能精確地表示為二進位制分數。在二進位制中,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.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') # 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。
>>> 0.1 + 0.1 + 0.1 == 0.3
False
此外,由於 0.1 無法更接近 1/10 的精確值,0.3 無法更接近 3/10 的精確值,因此使用 round()
函式進行預舍入也無濟於事
>>> round(0.1, 1) + round(0.1, 1) + round(0.1, 1) == round(0.3, 1)
False
儘管這些數字無法更接近其預期的精確值,但 math.isclose()
函式對於比較不精確的值很有用
>>> math.isclose(0.1 + 0.1 + 0.1, 0.3)
True
或者,可以使用 round()
函式來比較粗略的近似值
>>> round(math.pi, ndigits=2) == round(22 / 7, ndigits=2)
True
二進位制浮點算術有很多這樣的意外。下面“表示誤差”部分詳細解釋了“0.1”問題。有關二進位制浮點數如何工作以及實踐中常見的各種問題的愉快總結,請參閱 浮點數問題示例。另請參閱 浮點數陷阱,以獲取更完整的其他常見意外的描述。
正如其接近結尾處所說,“沒有簡單的答案。”不過,不要過分擔心浮點數!Python 浮點運算中的誤差繼承自浮點硬體,在大多數機器上,每次運算的誤差不超過 2**53 分之一。這對於大多數任務來說是足夠的,但您確實需要記住這不是十進位制算術,並且每次浮點運算都可能出現新的舍入誤差。
雖然存在病態情況,但對於大多數浮點算術的隨意使用,如果您只是將最終結果的顯示四捨五入到您期望的十進位制位數,您最終會看到您期望的結果。str()
通常就足夠了,如需更精細的控制,請參閱 格式字串語法 中 str.format()
方法的格式說明符。
對於需要精確十進位制表示的用例,請嘗試使用 decimal
模組,該模組實現了適用於會計應用和高精度應用的十進位制算術。
另一種形式的精確算術由 fractions
模組支援,該模組實現了基於有理數的算術(因此像 1/3 這樣的數字可以精確表示)。
如果您是浮點運算的重度使用者,您應該檢視 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)交換資料很有用。
另一個有用的工具是 sum()
函式,它有助於減輕求和過程中精度損失。它在將值加到執行總和中時使用擴充套件精度進行中間舍入步驟。這可以改變整體精度,使誤差不會累積到影響最終總和的程度
>>> 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 == 1.0
False
>>> sum([0.1] * 10) == 1.0
True
math.fsum()
更進一步,在將值加到執行總和中時跟蹤所有“丟失的數字”,以便結果只有一次舍入。這比 sum()
慢,但在大數值輸入相互抵消,最終總和接近零的罕見情況下會更準確
>>> arr = [-0.10430216751806065, -266310978.67179024, 143401161448607.16,
... -143401161400469.7, 266262841.31058735, -0.003244936839808227]
>>> float(sum(map(Fraction, arr))) # Exact summation with single rounding
8.042173697819788e-13
>>> math.fsum(arr) # Single rounding
8.042173697819788e-13
>>> sum(arr) # Multiple roundings in extended precision
8.042178034628478e-13
>>> total = 0.0
>>> for x in arr:
... total += x # Multiple roundings in standard precision
...
>>> total # Straight addition has no correct digits!
-0.0051575902860057365
15.1. 表示誤差¶
本節詳細解釋了“0.1”示例,並展示瞭如何自行對這類情況進行精確分析。假設您對二進位制浮點表示法有基本的瞭解。
**表示誤差**指的是某些(實際上是大多數)十進位制分數不能精確表示為二進位制(基數 2)分數的事實。這是 Python(或 Perl、C、C++、Java、Fortran 等許多其他語言)通常不會顯示您期望的精確十進位制數的主要原因。
為什麼會這樣?1/10 不能精確地表示為二進位制分數。自 2000 年以來,幾乎所有機器都使用 IEEE 754 二進位制浮點算術,並且幾乎所有平臺都將 Python 浮點數對映到 IEEE 754 binary64“雙精度”值。IEEE 754 binary64 值包含 53 位精度,因此在輸入時,計算機努力將 0.1 轉換為它能得到的最近似的分數 J/2**N,其中 J 是一個正好包含 53 位的整數。重寫
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
也就是說,56 是唯一一個能使 J 恰好有 53 位的 N 值。那麼 J 的最佳值就是該商的四捨五入值
>>> q, r = divmod(2**56, 10)
>>> r
6
由於餘數大於 10 的一半,因此透過向上舍入獲得最佳近似值
>>> q+1
7205759403792794
因此,在 IEEE 754 雙精度中,1/10 的最佳近似值是
7205759403792794 / 2 ** 56
分子和分母同時除以二,將分數簡化為
3602879701896397 / 2 ** 55
請注意,由於我們向上舍入了,這實際上比 1/10 略大;如果我們沒有向上舍入,商將比 1/10 略小。但在任何情況下都不能**精確**等於 1/10!
因此,計算機從不“看到”1/10:它看到的是上面給出的精確分數,即它能得到的最佳 IEEE 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'
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'