Python 的浮點數損失精度問題(爲何說雙精度浮點數有15位十進制精度)

本篇討論的現象能夠從下面這段腳本體現出來:編程

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

即:爲何有幾行的輸出看起來不對?spa

由於 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 入」,因此有時候會稍大一點有時候會稍小一點。.net

Python 的浮點數類型有一個 .hex()方法,調用可以返回該浮點數的二進制浮點數格式的十六進制版本。這話聽着有點繞,實際上是這樣的:原本浮點數應該有一個 .bin() 方法,用來返回其二進制浮點數格式。若是該方法存在的話,它看起來就像這樣(p-4表示乘以 2-4,或者能夠簡單理解爲小數點 左移 4 位):code

  
>>> ( 0.1 ).bin() # 本方法其實並不存在 ' 1.1001100110011001100110011001100110011001100110011010p-4 '

可是這個字符串太長了,同時由於每 4 位二進制字符均可以換算成 1 位十六進制字符,因而Python就放棄了給浮點數提供 .bin() 方法,改成提供 .hex() 方法。這個方法將上面輸出字符串的 52 位有效數字無損轉換成了 13 位十六進制數字,因此實際存在的方法實際上是這樣的(注:二進制浮點數中小數點前的「1」不包含於那 52 位有效數字之中):orm

  
>>> ( 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 這個數時,都會這麼寫「0x3.0p+0」。可是 Python 會這麼寫「0x1.8p+1」,即「1.1000」小數點右移一位變成「11.000」——確實仍是 3.0 。就是由於這個 1 是直接遺傳自二進制格式的。而我一開始沒有理解這個 .hex() 的意義,還多此一舉地自定義了一個 hex2bin() 方法,後來看看真是不必啊~ip

而爲了迴應人們在某些情況下對這個精度問題難以忍受的心情(霧),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() 的方法,就能夠返回這個浮點數的最簡分數表示,以一個元組的形式:ci

  
>>> ( 0.5 ).as_integer_ratio() ( 1 , 2 )

而後爲了對有理數套用更直觀的「位值法」表示形式,人們又開始用無限小數的形式表示有理數。而其中從某一位開始後面全是 0 的特殊狀況,被稱爲有限小數(沒錯,無限小數纔是本體)。但由於不少時候咱們只須要有限位有效數字的精度就夠用了,因此咱們會將有理數保存到某一位小數便截止。後面多餘小數的舍入方式即是「四捨五入」,這種方式較直接截斷(round_floor)的偏差更小。在二進制中,它表現爲「0 舍 1 入」。當咱們舍入到某一位之後,咱們就能夠說該數精確到了那一位。若是仔細體會每一位數字的含義就會發現,在以求得有限小數位下儘量精確的值爲目的狀況下,直接截斷的舍入方式其實毫無心義,獲得的那最後一位小數也並不許確。例如,將 0.06 舍入成 0.1 是精確到小數點後一位,而把它舍入成 0.0 就不算。所以,不管是在雙精度浮點數保留 52 位有效數字的時候,仍是從雙精度浮點數轉換回十進制小數並保留若干位有效數字的時候,對於最後一位有效數字,都是須要舍入的。字符串

插一句題外話:如何判斷一個有理數在寫成某種進制的小數時是否具備有限長度。就是看這個有理數的分母的質因子,是否所有包含於進制的質因子之中。舉個栗子,1/2 這個數在二進制、四進制和八進制...中都是有限小數,但在三進制、五進制和七進制...中都是無限小數。一樣道理,十進制下 1/n (n爲整數,0<n<10) 這種形式的有理數只有 1/2,1/4,1/8 和 1/5 是有限小數。所以,曾經還有過數學家提議人類使用十二進制代替十進制的事情,由於 12 的質因子(2,3)比 10 的質因子(2,5)更小,也就能夠表示更多的有限小數。

下圖是一個(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 位十進制小數將此區間劃分的更細,即精度更高。

未標題-1_thumb[4]

把 0.1 的雙精度版本(0x1.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 位的(我從別處看來的,C++我本身並不熟悉)。因此若是 Python 的 float 類型的 __str__() 和 __repr__() 方法選擇返回一個 15 位的小數,那麼就不會出現本文討論的第一個問題了。不管是早期的「0.10000000000000001」仍是本文中出現的「0.30000000000000004」或者「0.7999999999999999」,咱們能夠看到它的不精確都是由於打印了過多位的有效數字——16 或 17 。假如強制 round 到 15 位的話:

  
a = 0.1 for i in range( 10 ): print (round(a, 15 )) a += 0.1 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0

咱們再來看一下他的 1六、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 離的很近。所以通常咱們能夠經過這個 10:3 的比例來近似推導精度關係。

最後,由於浮點數的這點特性,在涉及到錢的地方都是不用浮點數的,他們會用定點數。即便用 ...xxxxx . xx 的格式存儲數字,精確到小數點後第二位,即「分」。

相關文章
相關標籤/搜索