浮點(diǎn)數(shù)在計(jì)算機(jī)中表達(dá)為二進(jìn)制(binary)小數(shù)。例如:十進(jìn)制小數(shù):
0.125
是 1/10 + 2/100 + 5/1000 的值,同樣二進(jìn)制小數(shù):
0.001
是 0/2 + 0/4 + 1/8。這兩個(gè)數(shù)值相同。唯一的實(shí)質(zhì)區(qū)別是第一個(gè)寫為十進(jìn)制小數(shù)記法,第二個(gè)是二進(jìn)制。
不幸的是,大多數(shù)十進(jìn)制小數(shù)不能完全用二進(jìn)制小數(shù)表示。結(jié)果是,一般情況下,你輸入的十進(jìn)制浮點(diǎn)數(shù)僅由實(shí)際存儲(chǔ)在計(jì)算機(jī)中的近似的二進(jìn)制浮點(diǎn)數(shù)表示。
這個(gè)問(wèn)題更早的時(shí)候首先在十進(jìn)制中發(fā)現(xiàn)??紤]小數(shù)形式的 1/3 ,你可以來(lái)個(gè)十進(jìn)制的近似值。
0.3
或者更進(jìn)一步的,
0.33
或者更進(jìn)一步的,
0.333
諸如此類。如果你寫多少位,這個(gè)結(jié)果永遠(yuǎn)不是精確的 1/3 ,但是可以無(wú)限接近 1/3 。
同樣,無(wú)論在二進(jìn)制中寫多少位,十進(jìn)制數(shù) 0.1 都不能精確表達(dá)為二進(jìn)制小數(shù)。二進(jìn)制來(lái)表達(dá) 1/10 是一個(gè)無(wú)限循環(huán)小數(shù):
0.0001100110011001100110011001100110011001100110011...
在任何有限數(shù)量的位停下來(lái),你得到的都是近似值。今天在大多數(shù)機(jī)器上,浮點(diǎn)數(shù)的近似使用的小數(shù)以最高的 53 位為分子,2 的冪為分母。至于 1/10 這種情況,其二進(jìn)制小數(shù)是?3602879701896397?/?2?**?55,它非常接近但不完全等于1/10真實(shí)的值。
由于顯示方式的原因,許多使用者意識(shí)不到是近似值。Python 只打印機(jī)器中存儲(chǔ)的二進(jìn)制值的十進(jìn)制近似值。在大多數(shù)機(jī)器上,如果 Python 要打印 0.1 存儲(chǔ)的二進(jìn)制的真正近似值,將會(huì)顯示:
>>> 0.1 0.1000000000000000055511151231257827021181583404541015625
這么多位的數(shù)字對(duì)大多數(shù)人是沒有用的,所以 Python 顯示一個(gè)舍入的值
>>> 1?/?10 0.1
只要記住即使打印的結(jié)果看上去是精確的 1/10,真正存儲(chǔ)的值是最近似的二進(jìn)制小數(shù)。
有趣地是,存在許多不同的十進(jìn)制數(shù)共享著相同的近似二進(jìn)制小數(shù)。例如,數(shù)字?0.1?和?0.10000000000000001?以及?0.1000000000000000055511151231257827021181583404541015625?都是?3602879701896397?/?2?**?55?的近似值。因?yàn)樗羞@些十進(jìn)制數(shù)共享相同的近似值,在保持恒等式?eval(repr(x))?==?x?的同時(shí),顯示的可能是它們中的任何一個(gè)。
歷史上,Python 提示符和內(nèi)置的?repr()?函數(shù)選擇一個(gè) 17 位精度的數(shù)字,0.10000000000000001。從 Python 3.1 開始,Python(在大多數(shù)系統(tǒng)上)能夠從這些數(shù)字當(dāng)中選擇最短的一個(gè)并簡(jiǎn)單地顯示?0.1。
注意,這是二進(jìn)制浮點(diǎn)數(shù)的自然性質(zhì):它不是 Python 中的一個(gè) bug,也不是你的代碼中的 bug。你會(huì)看到所有支持硬件浮點(diǎn)數(shù)算法的語(yǔ)言都會(huì)有這個(gè)現(xiàn)象(盡管有些語(yǔ)言默認(rèn)情況下或者在所有輸出模式下可能不會(huì)?顯示?出差異)。
為了輸出更好看,你可能想用字符串格式化來(lái)生成固定位數(shù)的有效數(shù)字:
>>> 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'
認(rèn)識(shí)到這,在真正意義上,是一種錯(cuò)覺是很重要的:你在簡(jiǎn)單地舍入真實(shí)機(jī)器值的?顯示。
例如,既然 0.1 不是精確的 1/10,3 個(gè) 0.1 的值相加可能也不會(huì)得到精確的 0.3:
>>> .1 + .1 + .1 == .3
False
另外,既然 0.1 不能更接近 1/10 的精確值而且 0.3 不能更接近 3/10 的精確值,使用?round()?函數(shù)提前舍入也沒有幫助:
>>> round(.1, 1) + round(.1, 1) + round(.1, 1) == round(.3, 1)
False
雖然這些數(shù)字不可能再更接近它們想要的精確值,round()?函數(shù)可以用于在計(jì)算之后進(jìn)行舍入,這樣的話不精確的結(jié)果就可以和另外一個(gè)相比較了:
>>> round(.1 + .1 + .1, 10) == round(.3, 10)
True
二進(jìn)制浮點(diǎn)數(shù)計(jì)算有很多這樣意想不到的結(jié)果?!?.1”的問(wèn)題在下面”誤差的表示”一節(jié)中有準(zhǔn)確詳細(xì)的解釋。更完整的常見怪異現(xiàn)象請(qǐng)參見?浮點(diǎn)數(shù)的危險(xiǎn)。
最后我要說(shuō),“沒有簡(jiǎn)單的答案”。也不要過(guò)分小心浮點(diǎn)數(shù)!Python 浮點(diǎn)數(shù)計(jì)算中的誤差源之于浮點(diǎn)數(shù)硬件,大多數(shù)機(jī)器上每次計(jì)算誤差不超過(guò) 2**53 分之一。對(duì)于大多數(shù)任務(wù)這已經(jīng)足夠了,但是你要在心中記住這不是十進(jìn)制算法,每個(gè)浮點(diǎn)數(shù)計(jì)算可能會(huì)帶來(lái)一個(gè)新的舍入錯(cuò)誤。
雖然確實(shí)有問(wèn)題存在,對(duì)于大多數(shù)平常的浮點(diǎn)數(shù)運(yùn)算,你只要簡(jiǎn)單地將最終顯示的結(jié)果舍入到你期望的十進(jìn)制位數(shù),你就會(huì)得到你期望的最終結(jié)果。str()?通常已經(jīng)足夠用了,對(duì)于更好的控制可以參閱?格式化字符串語(yǔ)法?中?str.format()?方法的格式說(shuō)明符。
對(duì)于需要精確十進(jìn)制表示的情況,可以嘗試使用?decimal?模塊,它實(shí)現(xiàn)的十進(jìn)制運(yùn)算適合會(huì)計(jì)方面的應(yīng)用和高精度要求的應(yīng)用。
fractions?模塊支持另外一種形式的運(yùn)算,它實(shí)現(xiàn)的運(yùn)算基于有理數(shù)(因此像1/3這樣的數(shù)字可以精確地表示)。
如果你是浮點(diǎn)數(shù)操作的重度使用者,你應(yīng)該看一下由 SciPy 項(xiàng)目提供的 Numerical Python 包和其它用于數(shù)學(xué)和統(tǒng)計(jì)學(xué)的包。參看 <http://scipy.org>。
當(dāng)你真的?真?想要知道浮點(diǎn)數(shù)精確值的時(shí)候,Python 提供這樣的工具可以幫助你。float.as_integer_ratio()?方法以分?jǐn)?shù)的形式表示一個(gè)浮點(diǎn)數(shù)的值:
>>> x = 3.14159
>>> x.as_integer_ratio()
(3537115888337719, 1125899906842624)
因?yàn)楸戎凳蔷_的,它可以用來(lái)無(wú)損地重新生成初始值:
>>> x == 3537115888337719 / 1125899906842624
True
float.hex()?方法以十六進(jìn)制表示浮點(diǎn)數(shù),給出的同樣是計(jì)算機(jī)存儲(chǔ)的精確值:
>>> x.hex()
'0x1.921f9f01b866ep+1'
精確的十六進(jìn)制表示可以用來(lái)準(zhǔn)確地重新構(gòu)建浮點(diǎn)數(shù):
>>> x == float.fromhex('0x1.921f9f01b866ep+1')
True
因?yàn)榭梢跃_表示,所以可以用在不同版本的 Python(與平臺(tái)相關(guān))之間可靠地移植數(shù)據(jù)以及與支持同樣格式的其它語(yǔ)言(例如 Java 和 C99)交換數(shù)據(jù)。
另外一個(gè)有用的工具是?math.fsum()?函數(shù),它幫助求和過(guò)程中減少精度的損失。當(dāng)數(shù)值在不停地相加的時(shí)候,它會(huì)跟蹤“丟棄的數(shù)字”。這可以給總體的準(zhǔn)確度帶來(lái)不同,以至于錯(cuò)誤不會(huì)累積到影響最終結(jié)果的點(diǎn):
>>> sum([0.1] * 10) == 1.0
False
>>> math.fsum([0.1] * 10) == 1.0
True
表達(dá)錯(cuò)誤
這一節(jié)詳細(xì)說(shuō)明 “0.1” 示例,教你怎樣自己去精確的分析此類案例。假設(shè)這里你已經(jīng)對(duì)浮點(diǎn)數(shù)表示有基本的了解。
Representation error?提及事實(shí)上有些(實(shí)際是大多數(shù))十進(jìn)制小數(shù)不能精確的表示為二進(jìn)制小數(shù)。這是 Python (或 Perl,C,C++,Java,F(xiàn)ortran 以及其它很多)語(yǔ)言往往不能按你期待的樣子顯示十進(jìn)制數(shù)值的根本原因。
這是為什么? 1/10 不能精確的表示為二進(jìn)制小數(shù)。大多數(shù)今天的機(jī)器(2000年十一月)使用 IEEE-754 浮點(diǎn)數(shù)算法,大多數(shù)平臺(tái)上 Python 將浮點(diǎn)數(shù)映射為 IEEE-754 “雙精度浮點(diǎn)數(shù)”。754 雙精度包含 53 位精度,所以計(jì)算機(jī)努力將輸入的 0.1 轉(zhuǎn)為?J/2**N?最接近的二進(jìn)制小數(shù)。J?是一個(gè) 53 位的整數(shù)。改寫:
1 / 10 ~= J / (2**N)
為:
J ~= 2**N / 10
J重現(xiàn)時(shí)正是 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
因?yàn)橛鄶?shù)大于 10 的一半,最好的近似是取上界:
>>> q+1
7205759403792794
因此在 754 雙精度中 1/10 最好的近似值是是 2**56,或:
7205759403792794 / 2 ** 56
分子和分母都除以2將小數(shù)縮小到:
3602879701896397 / 2 ** 55
要注意因?yàn)槲覀兿蛏仙崛?,它其?shí)比 1/10 稍大一點(diǎn)點(diǎn)。如果我們沒有向上舍入,它會(huì)比 1/10 稍小一點(diǎn)。但是沒辦法讓它?恰好?是 1/10!
所以計(jì)算機(jī)永遠(yuǎn)也不 “知道” 1/10:它遇到上面這個(gè)小數(shù),給出它所能得到的最佳的 754 雙精度實(shí)數(shù):
>>> .1 * 2**55
7205759403792794.0
如果我們把這小數(shù)乘以 10**55,我們可以看到其55位十進(jìn)制數(shù)的值:
>>> 3602879701896397 * 10 ** 55 // 2 ** 55
1000000000000000055511151231257827021181583404541015625
這表示存儲(chǔ)在計(jì)算機(jī)中的實(shí)際值近似等于十進(jìn)制值 0.1000000000000000055511151231257827021181583404541015625。許多語(yǔ)言(包括舊版本的Python)會(huì)把結(jié)果舍入到 17 位有效數(shù)字,而不是顯示全部的十進(jìn)制值:
>>> format(0.1, '.17f')
'0.10000000000000001'
fractions和decimal模塊使得這些計(jì)算很簡(jiǎn)單:
>>> 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'