首頁 > Python教程 > Python基礎教程 > Python入門教程之浮點數算法:爭議和限制

Python入門教程之浮點數算法:爭議和限制

時間:2019-08-21    來源:互聯網

15. 浮點數算法:爭議和限制

浮點數在計算機中表達為二進制(binary)小數。例如:十進制小數:

 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 位為分子,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,3 個 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”的問題在下面”誤差的表示”一節中有準確詳細的解釋。更完整的常見怪異現象請參見 浮點數的危險

最后我要說,“沒有簡單的答案”。也不要過分小心浮點數!Python 浮點數計算中的誤差源之于浮點數硬件,大多數機器上每次計算誤差不超過 2**53 分之一。對于大多數任務這已經足夠了,但是你要在心中記住這不是十進制算法,每個浮點數計算可能會帶來一個新的舍入錯誤。

雖然確實有問題存在,對于大多數平常的浮點數運算,你只要簡單地將最終顯示的結果舍入到你期望的十進制位數,你就會得到你期望的最終結果。str() 通常已經足夠用了,對于更好的控制可以參閱 格式化字符串語法 中 str.format() 方法的格式說明符。

對于需要精確十進制表示的情況,可以嘗試使用 decimal 模塊,它實現的十進制運算適合會計方面的應用和高精度要求的應用。

fractions 模塊支持另外一種形式的運算,它實現的運算基于有理數(因此像1/3這樣的數字可以精確地表示)。

如果你是浮點數操作的重度使用者,你應該看一下由 SciPy 項目提供的 Numerical Python 包和其它用于數學和統計學的包。參看 <http://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” 示例,教你怎樣自己去精確的分析此類案例。假設這里你已經對浮點數表示有基本的了解。

Representation error 提及事實上有些(實際是大多數)十進制小數不能精確的表示為二進制小數。這是 Python (或 Perl,C,C++,Java,Fortran 以及其它很多)語言往往不能按你期待的樣子顯示十進制數值的根本原因。

這是為什么? 1/10 不能精確的表示為二進制小數。大多數今天的機器(2000年十一月)使用 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 是保持 J 精度的唯一 N 值。J 最好的近似值是整除的商:

 >>> q, r = divmod(2**56, 10)  >>> r  6  

因為余數大于 10 的一半,最好的近似是取上界:

 >>> q+1  7205759403792794  

因此在 754 雙精度中 1/10 最好的近似值是是 2**56,或:

 7205759403792794 / 2 ** 56  

分子和分母都除以2將小數縮小到:

 3602879701896397 / 2 ** 55  

要注意因為我們向上舍入,它其實比 1/10 稍大一點點。如果我們沒有向上舍入,它會比 1/10 稍小一點。但是沒辦法讓它 恰好 是 1/10!

所以計算機永遠也不 “知道” 1/10:它遇到上面這個小數,給出它所能得到的最佳的 754 雙精度實數:

 >>> .1 * 2**55  7205759403792794.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'

 

相關推薦
Python入門簡介 Python能做什么?
調用Python 解釋器 什么是Python 解釋器?
Python3 簡介
Python入門教程之流程控制
Python入門教程之數據結構
Python入門教程之模塊
Python入門教程之輸入和輸出
Python入門教程之錯誤和異常處理
Python入門教程之類
Python入門教程之標準庫概覽
Python入門教程之標準庫瀏覽 – Part II
Python入門教程之虛擬環境和包

精彩推薦

熱門教程

上海天天彩选4第2018348期 甘肃省快3开奖结果一定牛 广东快乐十分100期 股票行情走势图 股城模拟炒股网页 国际棋牌送28元 天天玩麻将作弊器 九游旧版下载安装不更新 22选5开奖号码走势图 浙江6十1开奖号码查询 下载追光娱乐棋牌 娱乐棋牌app 一分赛车全天计划稳 3d每天开机号 快乐12开奖结果走 天津麻将胡牌牌型图解 甘肃快三一定