本篇討論的現象可以從下面這段腳本體現出來:
>>> x = 0.0 >>> for i in range(10): x += 0.1 print(x) 0.1 0.2 0.30000000000000004 0.4 0.5 0.6 0.7 0.7999999999999999 0.8999999999999999 0.9999999999999999 >>>
即:為什么有幾行的輸出看起來不對?
因為 Python 中使用雙精度浮點數來存儲小數。在 Python 使用的 IEEE 754 標准(52M/11E/1S)中,8字節64位存儲空間分配了52位來存儲浮點數的有效數字,11位存儲指數,1位存儲正負號,即這是一種二進制版的科學計數法格式。雖然52位有效數字看起來很多,但麻煩之處在於,二進制小數在表示有理數時極易遇到無限循環的問題。其中很多在十進制小數中是有限的,比如十進制的 1/10,在十進制中可以簡單寫為 0.1 ,但在二進制中,他得寫成:0.0001100110011001100110011001100110011001100110011001…..(后面全是 1001 循環)。因為浮點數只有52位有效數字,從第53位開始,就舍入了。這樣就造成了標題里提到的”浮點數精度損失“問題。 舍入(round)的規則為“0 舍 1 入”,所以有時候會稍大一點有時候會稍小一點。
Python 的浮點數類型有一個 .hex()方法,調用能夠返回該浮點數的二進制浮點數格式的十六進制版本。這話聽着有點繞,其實是這樣的:本來浮點數應該有一個 .bin() 方法,用來返回其二進制浮點數格式。如果該方法存在的話,它看起來就像這樣(p-4表示×2-4,或者可以簡單理解為小數點 左移 4 位):
>>> (0.1).bin()#本方法其實並不存在 '1.1001100110011001100110011001100110011001100110011010p-4'
但是這個字符串太長了,同時因為每 4 位二進制字符都可以換算成 1 位十六進制字符,於是Python就放棄了給浮點數提供 .bin() 方法,改為提供 .hex() 方法。這個方法將上面輸出字符串的 52 位有效數字轉換成了 13 位十六進制數字,所以該方法用起來其實是這樣的(注:二進制浮點數中小數點前的“1”不包含於那 52 位有效數字之中):
>>> (0.1).hex() '0x1.999999999999ap-4'
前面的 0x 代表十六進制。p-4 沒變,所以需要注意,這里的 p-4 還是二進制版的,也就是說在展開本格式的時候,你不能把小數點往左移 4 位,那樣就相當於二進制左移 16 位了。前面提到過,小數點前這個“1”是不包含於 52 位有效數字之中的,但它確實是一個有效的數字呀,這是因為,在二進制浮點數中,第一位肯定是“1”,(是“0”的話就去掉這位,並在指數上-1)所以就不保存了,這里返回的這個“1”,是為了讓人看懂而加上的,在內存的 8 位空間中並沒有它。所以 .hex() 方法在做進制轉換的時候,就沒有顧慮到這個“1”,直接把 52 位二進制有效數字轉換掉就按着原來的格式返回了。因此這個 .hex() 方法即使名義上返回的是一個十六進制數,它小數點前的那一位也永遠是“1”,看下面示例:
>>> float.fromhex('0x1.8p+1') == float.fromhex('0x3.0p+0') True
一般我們用十六進制科學計數法來表示 3.0 這個數時,都會這么寫“0×3.0p+0”。但是 Python 會這么寫“0×1.8p+1”,即“1.1000”小數點右移一位變成“11.000”——確實還是 3.0 。就是因為這個 1 是直接遺傳自二進制格式的。而我一開始沒有理解這個 .hex() 的意義,還畫蛇添足地自定義了一個 hex2bin() 方法,后來看看真是沒必要啊~
而為了回應人們在某些狀況下對這個精度問題難以忍受的心情(霧),Python 提供了另一種數字類型——Decimal 。他並不是內建的,因此使用它的時候需要 import decimal 模塊,並使用 decimal.Decimal() 來存儲精確的數字。這里需要注意的是:使用非整數參數時要記得傳入一個字符串而不是浮點數,否則在作為參數的時候,這個值可能就已經是不精確的了:
>>> Decimal(0.1) == Decimal('0.1') False
在進一步研究到底損失了多少精度,或者說,八字節浮點數最多可以達到多少精度的問題之前,先來整理一下小數和精度的概念。本篇中討論的小數問題僅限於有理數范圍,其實有理數也是日常編程中最常用到的數。有理數(rational number)一詞派生於“比(ratio)”,因此並不是指“有道理”的意思。有理數的內容擴展自自然數,由自然數通過有理運算(+ – * /)來得到的數系稱為有理數,因此可以看到它較自然數擴充了:零、負整數和分數的部分。有理數總可以寫成 p/q 的形式,其中 p、q 是整數且 q ≠ 0,而且當 p 和 q 沒有大於 1 的公因子且 q 是正數的時候,這種表示法就是唯一的。這也就是有理數被稱為 rational number 的原因,說白了就是分數。實際上 Python 的 float 類型還有一個 .as_integer_ratio() 的方法,就可以返回這個浮點數的最簡分數表示,以一個元組的形式:
>>> (0.5).as_integer_ratio() (1, 2)
然后為了更直觀地表現,人們又開始用無限小數的形式表示有理數(分數)。而其中從某一位開始后面全是 0 的特殊情況,被稱為有限小數(沒錯,無限小數才是本體)。但因為很多時候我們並不需要無限長的小數位,我們會將有理數保存到某一位小數便截止了。后面多余小數的舍入方式便是“四舍五入”,這種方式較直接截斷(round_floor)的誤差更小。在二進制中,它表現為“0 舍 1 入”。當我們舍入到某一位以后,我們就可以說該數精確到了那一位。如果仔細體會每一位數字的含義就會發現,在以求得有限小數位下盡可能精確的值為目的情況下,直接截斷的舍入方式其實毫無意義,得到的那最后一位小數也並不精確。例如,將 0.06 舍入成 0.1 是精確到小數點后一位,而把它舍入成 0.0 就不算。因此,不論是在雙精度浮點數保留 52 位有效數字的時候,還是從雙精度浮點數轉換回十進制小數並保留若干位有效數字的時候,對於最后一位有效數字,都是需要舍入的。
下圖是一個(0,1)之間的數軸,上面用二進制分割,下面用十進制分割。比如二進制的 0.1011 這個數,從小數點后一位一位的來看每個數字的意義:開頭的 1 代表真值位於 0.1 的右側,接下來的 0 代表真值位於 0.11 的左側,再接下來的 1 代表真值位於 0.101 的右側,最后的 1 代表真值位於 0.1011 的右側(包含正好落在 0.1011 上這種情況)。使用 4 位二進制小數表示的 16 個不同的值,除去 0,剩下的 15 個數字正好可以平均分布在(0,1)這個區間上,而十進制只能平均分布 9 個數字。顯然 4 位二進制小數較於 1 位十進制小數將此區間划分的更細,即精度更高。
把 0.1 的雙精度版本(0×1.999999999999ap-4)展開成十進制。這里使用了 Decimal 類型,在給他賦值的時候,他會完整存儲參數,但是要注意的是,使用 Decimal 進行運算是會舍入的,保留的位數由上下文決定。使用 decimal 模塊的 getcontext() 方法可以得到上下文對象,其中的 prec 屬性就是精度。下面還使用了 print() 方法,這是為了好看:
>>> print(Decimal(0.1)) 0.1000000000000000055511151231257827021181583404541015625
得到的這個十進制浮點數有效數字足有 55 位。雖然從二進制到十進制這個過程是完全精確的,但因為在存儲這個二進制浮點數的時候進行了舍入,所以這個 55 位的十進制數,較於最初的 0.1 並不精確。至於到底能精確到原十進制數的哪一位,可以這么算: 2**53 = 9007199254740992 ≈ 10**16 ,(這里 53 算上了開頭的“1”),即轉換后的十進制小數的第 16 位有效數字很可能是精確的(第 15 位肯定是精確的)。換句話說,如果要唯一表示一個 53 位二進制數,我們需要一個 17 位的十進制數(但即使這樣,我們也不能說對應的十進制和二進制數完全相等,他們只不過在互相轉換的時候在特定精度下可以得到相同的的值罷了。就像上面例子中顯示的,精確表示”0.1“的雙精度版本,需要一個 55 位的十進制小數)。
不過可以看到,如果要保證轉換回來的十進制小數與原值相等,那么只能保證到 15 位,第 16 位只是“很可能是精確的”。而且第 15 位的精確度也要依賴於第 16 位的舍入。實際上在 C++ 中,我看到有別人講,double 類型的十進制小數就是保留 15 位的(這點我自己並不清楚)。所以如果 Python 的 float 類型的 __str__() 和 __repr__() 方法選擇返回一個 15 位的小數,那么就不會出現本文討論的第一個問題了。不論是早期的“0.10000000000000001”還是本文中出現的“0.30000000000000004”或者“0.7999999999999999”,我們可以看到它的不精確都是因為保存了過多位的有效數字,16 或 17 。從下面的腳本中可以看得更加清楚:
>>> a=0.0 >>> for i in range(10): a += 0.1 print(a) print('%.17f'%a) print('-'*19) 0.1 0.10000000000000001 ------------------- 0.2 0.20000000000000001 ------------------- 0.30000000000000004 0.30000000000000004 ------------------- 0.4 0.40000000000000002 ------------------- 0.5 0.50000000000000000 ------------------- 0.6 0.59999999999999998 ------------------- 0.7 0.69999999999999996 ------------------- 0.7999999999999999 0.79999999999999993 ------------------- 0.8999999999999999 0.89999999999999991 ------------------- 0.9999999999999999 0.99999999999999989 -------------------
上面短橫線對齊的是第 17 位。雖然在這里第 16 位全部是精確的,但如果為了保證 100% 的准確率的話,還是需要舍入到第 15 位。另外一個細節,上面的例子其實有一個問題,就是使用 0.1++ 這種方式的時候,實際累加的是一個不精確的數字,所以有可能造成誤差的放大。不過這里依然沒有改正,是因為 0.5 那行,突然恢復真值了。這也不是因為后面藏了其他數字沒有顯示出來,我們來看一下:
>>> '%.60f'%(0.1+0.1+0.1+0.1+0.1) '0.500000000000000000000000000000000000000000000000000000000000' >>> print(Decimal(0.1+0.1+0.1+0.1+0.1)) 0.5
這里使用了一個格式限定符的示例。它的作用類似於 print Decimal。區別僅在於 Decimal 自己知道應該顯示多少位,而格式化限定符不知道。(一般雙精度浮點數轉換過來不超過 100 位)。因為不打算繼續深究了,所以就當這個“0.5”是個意外吧~如果想避免誤差疊加,可以寫成“i/10”的格式。
所以對於兩種,不像十六進制和二進制般正好是指數關系的進制,永遠都無法在各自的某一位上具有相同的精度。即 2m = 10n 這個等式沒有使 m,n 同時為整數的解。但至少還可以構建一個精度包含的關系,比如上面 24 > 101 ,那么我們就說“4 位二進制精度高於 1 位十進制精度”從而通過 4 位二進制數轉儲 1 位十進制數的時候,總是精確的,反之則不然。同理根據這個不等式:1015 < 253 <1016 ,雙精度浮點數的精度最高也就蘊含(不是等價)到十進制的 15 位了。另外雖然這種轉化看起來浪費了很大的精度(第 16 位在很大概率上也是精確的),有趣的是,210 = 1024,卻和 103 = 1000 離的很近。因此一般我們可以通過這個關系來近似推導精度關系。