Python 為什麼不解決四捨五入(round)的「bug」?

使用 round() 這個函數:

&>&>&> i=1.5
&>&>&> ii=round(i,0)
&>&>&> ii
2.0
&>&>&> i=1.25
&>&>&> ii=round(i,1)
&>&>&> ii
1.2
&>&>&> i=1.245
&>&>&> ii=round(i,2)
&>&>&> ii
1.25
&>&>&> i=1.45
&>&>&> ii=round(i,1)
&>&>&> ii
1.4
&>&>&> i=1.415
&>&>&> ii=round(i,2)
&>&>&> ii
1.42

奇怪的是四捨五入的規則時而可以時而不行。API 中的解釋是:

Note

The behavior of round() for floats can be surprising: for example, round(2.675, 2) gives 2.67 instead of the expected 2.68. This is not a bug: it』s a result of the fact that most decimal fractions can』t be represented exactly as a float.

問題是,為什麼內置的庫函數 round 不解決下這個問題?


因為二進位浮點數不能解決這個問題。

先看一個現象,和 round 無關的:

&>&>&> def show(x):
... """列印一個數,20 位精度"""
... print("{:.20f}".format(x))
...
&>&>&> show(1.5)
1.50000000000000000000
&>&>&> show(1.25)
1.25000000000000000000
&>&>&> show(1.245)
1.24500000000000010658
&>&>&> show(1.45)
1.44999999999999995559
&>&>&> show(1.415)
1.41500000000000003553

從數學上看,一個既約分數(有理數)n/d 要表示為 B 進位數,如果 d 的所有素因子都整除 B,就說明存在一個整數 k,使得分母 d 整除 B^k——比如 q * d = B^k,於是此時有 n/d = n / (B^k / q) = nq / B^k,也就是說 n/d 可以使用至多 k 位數的 B 進位小數有限表示。

反之,也容易證明,如果 d 有素因子不能整數 B,那麼就不存在上面的 k,也就是說有理數 n/d 不可能由有限位數的 B 進位數表示。——也就是說會出現循環小數。

對於 10 進位數,所有分母只有素因子 2 和 5 的有理數,都能表示為有限小數。比如 1/2 是 0.5,1/4 是 0.25,1/5 是 0.2,3/8 是 0.375,都是有限的。

而對於 2 進位數,分母裡面只有素因子 2 的有理數,才能表示為有限小數。所以 1/2 是 0.1,1/4 是 0.01,3/8 是 0.011。但 1/5 就不能用有限二進位數表示了,是個循環小數 0.0011 0011 0011……。

(什麼,你問無理數?小學生都知道無理數是無限不循環小數。)

現在:

  • 計算機的浮點數是用小數表示的。浮點數就是科學計數法,指數部分是個整數,尾數部分是個小數。
  • 計算機的存儲是有限的,所以只能使用有限位數的小數表示。
  • 計算機硬體通常是用二進位運算的。

具體到 Python 的 float,C/C++ 的 float/double,都是有限長度的、二進位、浮點數。準確地說,是這個:IEEE floating point

現在問題來了。人寫程序用十進位數,計算機運行程序用二進位數,怎麼辦?轉換唄。

從概念上說,就是我寫:

a(10) = 0.5

計算機讀:

a(2) = 0.1

我寫:

b(10) = 0.375

計算機讀

b(2) = 0.011

可如果我寫

c(10) = 0.2

計算機就只能讀成了有限位數,比如 12 位:

c(2) = 0.0011 0011 0011(咔嚓切斷)

其實這個被截斷保留 12 位二制制位的數用十進位表示是 0.199951171875,已經不準了。你說不對呀這個小了,最後一位能不能加上去,讓計算機讀成

c(2) = 0.0011 0011 0100(進了一位)

這個數用十進位表示則是 0.2001953125,又大了。

——實際的計算機中,python 的 float 是用 64 位浮點數,其中 53 位是尾數部分,誤差小得多了,但還是不可能沒有。

所以,計算機在把人寫的程序轉換為內部代碼的時候,就必須做十進位到二進位的轉換。這個轉換就已經不得不帶來誤差了。當然,像前面說的,不是所有的數都有誤差,0.5、3.75、78.625 都不會有誤差,但簡單的 0.2、3.15 就會有進位轉換誤差。

所以,在開始計算 round 這個四捨五入的函數之前,在程序剛被讀入計算機時,這個變數的值早已經不精確了。round 又能解決什麼問題?

話說回來,round 本身在一些情況下是準確的。比如 0.5、1.5、2.5、3.5 這些數,都能用有限位二進位數表示,它們直接 round 的結果也都是準確的,不過使用的不是四捨五入而是無偏演算法(把 0.5 向偶數而不是向上舍入,這裡指 Python 3)。round 在另一些情況下又可能是不準確的,因為 Python 的 round 有兩個參數,第二個參數表示舍入到第幾位,就需要對原數先計算再舍入,就不準確了。

如果讓 Python 減慢速度,也在內部用十進位表示和計算,就不會出現進位轉換和移位這種來源的舍入誤差,此時做舍入運算或輸入輸出就是精確的了。雖然這不能防止其他計算(比如除法、三角函數)帶來的誤差,但在一些場合,比如金融算錢的時候,還是非常有用的。在 Python 裡面可以用 decimal 包來使用十進位浮點數,避免輸入輸出的帶來的進位轉換誤差,按十進位移位時除法帶來的誤差等。

小結:

  • 誤差主要來自輸入時十進位轉換為計算機內部二進位時,且這個問題在有限精度下不可能解決,也不需要解決。
  • round 可以準確舍入,但它涉及的移位計算也可能帶來其他誤差。
  • Python 的 decimal 包可用於解決這一問題。

(寫完發現是挖了個古墳,真無趣。)


四捨五入是基於十進位的,在二進位無法精確表示的時候是會有誤差的。

任何需要十進位運算的地方,都需要用 decimal.Decimal 取代 float:

&>&>&> Decimal(1.45)
Decimal("1.4499999999999999555910790149937383830547332763671875")
&>&>&> Decimal("1.45")
Decimal("1.45")
&>&>&> Context(prec=2, rounding=ROUND_HALF_UP).create_decimal("1.45")
Decimal("1.5")
&>&>&> Decimal("1.45").normalize(Context(prec=2, rounding=ROUND_HALF_UP))
Decimal("1.5")
&>&>&> Decimal(Decimal("1.45").quantize(Decimal(".1"), rounding=ROUND_HALF_UP))
Decimal("1.5")

不過使用十進位運算的代價就是慢,所以自己抉擇吧。


Note that this is in the very nature of binary floating-point: this is not a bug in Python, and it is not a bug in your code either. You』ll see the same kind of thing in all languages that support your hardware』s floating-point arithmetic (although some languages may not display the difference by default, or in all output modes).

  • 某些十進位數(如0.1)在機器內部無法用有限數目的二進位位0和1精確表示,只能通過增加固定的位數來提高精度,從而更逼近原來十進位數。

round(number[, ndigits])

Return the floating point value number rounded to ndigits digits after the decimal point. If ndigits is omitted, it defaults to zero. The result is a floating point number. Values are rounded to the closest multiple of 10 to the power minus ndigits; if two multiples are equally close, rounding is done away from 0.

  • python的round函數定義為 在任意整數*10^(-ndigits) 中取最靠近number的數值,如果有兩個整數距離number相等則按照遠離0的一側(負數-0-正數)取值 。

round(0.5) is 1.0 and round(-0.5) is -1.0)

有點繞,以round(1.45,1)為例。 任意整數*10^(-1) 最靠近1.45的有1.4(=14*0.1)和1.5(=15*0.1)。看起來二者離1.45距離相等均為0.05,其實1.45在計算機內部用二進位存儲的值對應的十進位數值為:

&>&>&> from decimal import Decimal
&>&>&> Decimal(2.675)
Decimal("1.4499999999999999555910790149937383830547332763671875")

所以此時按照round函數定義,取最靠近1.449999999999...的值自然就是1.4而不是1.5了。

其他的情況類似分析,具體細節參看:

  1. 14. Floating Point Arithmetic: Issues and Limitations

  2. Python 3.3.2 round函數並非"四捨五入"


題主測試的和那個bug無關。round是四捨六入五成雙的。


兩個問題,一個是

浮點數在計算機中的真實存儲(這裡是在 Python 中的存儲)

expect: 1.0
actual: 1
expect: 1.1
actual: 1.100000000000000088817841970012523233890533447265625
expect: 1.2
actual: 1.1999999999999999555910790149937383830547332763671875
expect: 1.3
actual: 1.3000000000000000444089209850062616169452667236328125
expect: 1.4
actual: 1.399999999999999911182158029987476766109466552734375
expect: 1.5
actual: 1.5
expect: 1.6
actual: 1.600000000000000088817841970012523233890533447265625
expect: 1.7
actual: 1.6999999999999999555910790149937383830547332763671875
expect: 1.8
actual: 1.8000000000000000444089209850062616169452667236328125
expect: 1.9
actual: 1.899999999999999911182158029987476766109466552734375

所以說我們真正給 round 傳的數本身就是有問題的啦。

Ref: https://docs.python.org/2/library/functions.html#round

想要給 round 傳真正的數,可以使用 Decimal 模塊

另一個問題,python 內置 round 的方式,和我們小學學的不同;)

所以需要用 Decimal 的 quantize

注意 Decimal 默認的四捨五入方式並不是我們所想的那種,根據具體的應用場景查看 rounding 設置即可

Ref: 9.4. decimal


python 的官方實現確實有問題,至於為什麼作者不改進,原作者可能是什麼心態,可以參見劉海洋的答案。大致心態也許是:反正這個錯誤不在我,所以雖然有方法,但我也不會去解決這個問題。

咋一看,很多人會把這個問題理解為浮點數不精確的問題。浮點數不精確,這一點是常識,是對的。

但浮點數並非在所有情況下都不精確,也並非「只要浮點數不精確,所涉及的相關計算問題就毫無解決的價值」。

Python 的 round 問題是個典型的例子,推測作者的觀點是認為這類與浮點數精確度有關的問題沒有解決的必要。如同 @劉海洋 的觀點一樣,所以導致了現在的結果。

不過仔細分析會發現,0.5 這種浮點數是可以被精確表示的,而 round 這個函數的特定性在於,round 舍入之後的精確性毫無意義。所以 round 這個問題本身造成的不精確性是可以被解決的。

對於 round 的不精確性,重要的在於結果是進一還是去尾。我寫了一個簡單的例子來說明這個問題,當然這個函數在特定情況下也不準確,不過在題目給出的情況下都可以得到正確的結果。

#include &

float my_round(float src, int idx)
{
int i;
for (i=idx;i--;)
src *=10;
float dest = (int)src;
if (src &>= dest+0.5)
dest += 1;
for (i=idx;i--;)
dest /=10;
return dest;
}

int main()
{
printf("result=%f
", my_round(1.5, 0));
printf("result=%f
", my_round(1.25, 1));
printf("result=%f
", my_round(1.245, 2));
printf("result=%f
", my_round(1.45, 1));
printf("result=%f
", my_round(1.415, 2));
printf("result=%f
", my_round(2.675, 2));
}

我們看到上面的例子中,結論都是正確的。換句話說,以上演算法在進一與去尾方面並沒有出現錯誤

認真研究這個問題的來源,涉及到設計上的取捨以及對演算法的理解。就是說有限的浮點數在缺少的位置上如何理解。比方說 1.4499999 這種數字,假定是因為 float 的精度原因不能保存更多的位數,那麼在精度之外缺少的位置是什麼?或者說,乘 10 的時候補什麼?——有說法認為應該無限補 9 ,有說法應該無限補 0 。——事實是,當我們讓 CPU 執行乘 10 的操作時,它可能選擇了補9,而從數學的角度,無限補9,這個數就成了 14.5。我們拿它跟 14.5 這種精確數比較大小,發現它大於等於 14.5。

其實大家有興趣的發現,在同樣的 double 範圍內的浮點計算中,python 與 lua 的計算結果可能不同,其原因是 lua 對精度進行了一些取捨,導致它在多數計算的結果中更符合大眾認知。——這個 round 的問題,為什麼乘 10 ,乘 100 可以解決小數點後的舍入誤差問題。實際上是 CPU 本身對浮點數進行的補償。換句話說,雖然 1.45 在保存時產生了誤差,但乘十的時候 CPU 試圖補償這種誤差。

一個簡單的例子可以證明 CPU 確實試圖補償:

&>&>&> show(f)
1.449999999999999955591079014994
&>&>&> show (f*10)
14.500000000000000000000000000000

最終,它在於你的選擇,是選擇 1 呢還是選擇 2 呢:

  1. 反正這個過程錯誤不在我,所以我也懶得解決這個問題,而且某種極端情況下我恰好是對的。
  2. 有某種方案能夠在多數情況下獲得令人期望的結果,雖然它可能造成另外一種極端情況下的問題,但它得到正確結果的概率比出錯的概率更大,因此我願意如此優化。

我覺得其實給 python 團隊提一個簡單的 patch 可以解決這個問題。——當然,流派之爭是很可怕的,如同 python 3 刻意的製造不兼容,也可以被認為是好的做法一樣。所以 patch 未必會被採納。


很簡單,因為2.675在表示的時候可能是2.6749,所以round以後還是2.67了


你的需求本質是:精確小數運算。然而,float不是為了滿足這一需求而設計的,decimal才是。所以,為float單獨定製一個round,不符合float的設計意圖,也很難實現。以你的函數為例,temp*10這個操作在float下不是精確的。

&>&>&> 1.222*10

12.219999999999999


相關問題:

http://www.zhihu.com/question/34497644

個人認為python這種開源的代碼庫有很多不足,但是人非聖賢孰能無過。不過設計語言的時候要講究符合人的常識,這樣不符合常理的做法徒然增加了學習的難度


為啥我覺得round()完美符合物理實驗對數據近似的要求…


我的意思是為啥不寫個可以解決這個問題的函數:例如(僅僅一個例子,可能從性能啊之類肯定不行,官方是出於什麼原因不寫一個類似的)

def myround(par,l):

temp = 1

for i in range(l):

temp*=10

v = int((par+0.5/temp)*temp) / temp

return v

i = 1.25

print(myround(i,1))

i = 1.245

print(myround(i,2))

i = 1.21

print(myround(i,1))

i = 1.249

print(myround(i,2))

----

1.3

1.25

1.2

1.25


他的解釋應該說的是這個不是個bug。默認數值都是float,float本來就不精確,所以python應該是只對decimal精確。我覺得這個也挺正常的。


推薦閱讀:

Python httplib2 如何設置超時?

TAG:Python | Python3x | Python文檔 |