python 中浮點數四捨五入的問題

昨天遇到一個問題,在 6.6045 保留三位小數時,使用 round() 函數進行計算,我們希望得到 6.605,然而:

round(6.6045, 3)
6.604

網上有人說,因爲在計算機裏面,小數是不精確的,例如 1.115 在計算機中實際上是 1.114999999999999991182,所以當你對這個小數精確到小數點後兩位的時候,實際上小數點後第三位是 4,所以四捨五入,結果爲 1.11.

這種說法,對了一半。

因爲並不是所有的小數在計算機中都是不精確的。例如 0.125 這個小數在計算機中就是精確的,它就是 0.125,沒有省略後面的值,沒有近似,它確確實實就是 0.125.

但是如果我們在 Python 中運行:

round(0.125, 2)
0.12

爲什麼在這裏四舍了?

還有更奇怪的,另一個在計算機裏面能夠精確表示的小數 0.375,我們來看看精確到小數點後兩位是多少:

round(0.375, 2)
0.38

爲什麼在這裏又五入了?

解析

因爲在 Python3 裏面,round 對小數的精確度採用了四捨六入五成雙的方式。

如果你寫過大學物理的實驗報告,那麼你應該會記得老師講過,直接使用四捨五入,最後的結果可能會偏高,所以需要使用奇進偶舍的處理方法。

例如對於一個浮點數 a.bcd,需要精確到小數點後兩位,那麼就要看小數點後第三位:
•如果 d 小於 5,直接捨去
•如果 d 大於 5,直接進位
•如果 d 等於 5:
• ◦d 後面沒有數據,且 c 爲偶數,那麼不進位,保留 c

• ◦d 後面沒有數據,且 c 爲奇數,那麼進位,c 變成 (c + 1)

• ◦如果 d 後面還有非 0 數字,例如實際上小數爲 a.bcdef,此時一定要進位,c 變成 (c + 1)

關於奇進偶舍,有興趣的朋友可以在維基百科搜索這兩個詞條:數值修約和奇進偶舍。

所以,round 給出的結果如果跟設想的不一樣,那麼需要考慮兩個原因:
1.你的這個小數在計算機中能不能被精確儲存?如果不能,那麼它可能並沒有達到四捨五入的標準,例如 1.115,它的小數點後第三位實際上是 4,當然會被捨去。
2.如果你的這個小數在計算機中能被精確表示,那麼,round 採用的進位機制是奇進偶舍,所以這取決於你要保留的那一位,它是奇數還是偶數,以及它的下一位後面還有沒有數據。

回到最開始的問題,對於 6.6045 這個浮點數,我們在 Scheme 中查看一下它的精確形式:

(exact 6.6045)
3718002967371055/562949953421312

也就是說它是不能被精確儲存的,大概表現爲 6.60449999999999…的形式,因此四捨五入的時候得到了 6.604。

如何正確進行四捨五入

如果要實現數學上的四捨五入,那麼就需要使用 decimal 模塊。

具體用法參考官方文檔:https://docs.python.org/zh-cn/3.7/library/decimal.html

其中 quantize 的函數原型和文檔說明,提到了可以通過指定 rounding 參數來確定進位方式。如果沒有指定 rounding 參數,那麼會默認使用上下文提供的進位方式。

現在我們來查看一下默認的上下文中的進位方式是什麼:

from decimal import getcontext
getcontext().rounding
'ROUND_HALF_EVEN'

ROUND_HALF_EVEN 實際上就是奇進偶舍,如果要指定真正的四捨五入,那麼我們需要在 quantize 中指定進位方式爲 ROUND_HALF_UP:

from decimal import Decimal, ROUND_HALF_UP
Decimal('0.125').quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
Decimal('0.13')

現在看起來一切都正常了。

有人可能會進一步追問一下,如果 Decimal 接收的參數不是字符串,而是浮點數會怎麼樣呢?

來實驗一下:

Decimal(0.125)
Decimal('0.125')

那是不是說明,在 Decimal 的第一個參數,可以直接傳浮點數呢?

我們換一個數來測試一下:

Decimal(11.245)
Decimal('11.2449999999999992184029906638897955417633056640625')

浮點數 11.245 和字符串’11.245’傳進去以後的結果居然不一樣。

我們繼續在文檔中尋找答案。
python 中浮點數四捨五入的問題

官方文檔已經很清楚地說明了,如果你傳入的參數爲浮點數,並且這個浮點值在計算機裏面不能被精確存儲,那麼它會先被轉換爲一個不精確的二進制值,然後再把這個不精確的二進制值轉換爲等效的十進制值。對於不能精確表示的小數,當你傳入的時候,Python 在拿到這個數前,這個數就已經被轉成了一個不精確的數了。所以雖然參數傳入的是 11.245,但是 Python 拿到的實際上是 11.24499999999…

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章