為什麼你需要少看垃圾部落格以及如何在Python裡精確地四捨五入

青南發表於2019-03-31

今天又有一個Python初學者被中文技術部落格中的垃圾文章給誤導了。

這位初學者的問題是:

在Python中,如何精確地進行浮點數的四捨五入,保留兩位小數?

如果你在Google或者百度上搜尋,你會發現大量的來自CSDN、百家號、頭條號或者簡書上面的文章講到這一點,但是他們的說法無外乎下面幾種:

連例子都不舉的垃圾文章

如下圖所示,懶得吐槽。

為什麼你需要少看垃圾部落格以及如何在Python裡精確地四捨五入

使用round函式

他們舉的例子為:

>>> round(1.234, 2)
1.23
複製程式碼

這種文章,他只演示了四舍,但是卻沒有演示五入。所以如果你程式碼稍作修改,就會發現有問題:

>>> round(11.245, 2)
11.24
複製程式碼

先放大再縮小

這種文章稍微好一點,知道多舉幾個例子:

為什麼你需要少看垃圾部落格以及如何在Python裡精確地四捨五入

然而這種文章也是漏洞百出,只要你多嘗試幾個數字就會發現問題,在Python 2和Python 3下面,效果是不一樣的。先來看看Python 2下面的執行效果:

為什麼你需要少看垃圾部落格以及如何在Python裡精確地四捨五入

在Python 2裡面,直接使用round1.125精確到兩位小數後為1.13,而1.115精確到兩位小數後是1.11

再來看看Python 3下面的效果:

為什麼你需要少看垃圾部落格以及如何在Python裡精確地四捨五入

在Python 3下面,1.125在精確到兩位小數以後是1.12

他舉的例子,在Python 3中先放大再縮小,也並不總是正確。

裝逼貨

還有一種裝逼貨,文章和先放大再縮小差不多,但是他還知道decimal這個模組。

不過他的使用方法,大家看他吧

為什麼你需要少看垃圾部落格以及如何在Python裡精確地四捨五入

具體原因不詳 ????

不推薦使用這個方法???

這種人要先裝個逼,表示自己知道有這樣一個庫,但是用起來發現有問題,而且不知道原因,所以不建議大家使用。

decimal是專門為高精度計算用的模組,他竟然說不建議大家使用???

為什麼你需要少看垃圾部落格以及如何在Python裡精確地四捨五入

round到底出了什麼問題?

罵完了,我們來說說,在Python 3裡面,round這個內建的函式到底有什麼問題。

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

這種說法,對了一半。

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

但是如果我們在Python中把0.125精確到小數點後兩位,那麼它的就會變成0.12

>>> round(0.125, 2)
0.12
複製程式碼

為什麼在這裡四舍了?

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

>>> round(0.375, 2)
0.38
複製程式碼

為什麼這裡又五入了?

因為在Python 3裡面,round對小數的精確度採用了四捨六入五成雙的方式。

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

例如對於一個小數a.bcd,需要精確到小數點後兩位,那麼就要看小數點後第三位:

  1. 如果d小於5,直接捨去
  2. 如果d大於5,直接進位
  3. 如果d等於5:
    1. d後面沒有資料,且c為偶數,那麼不進位,保留c
    2. d後面沒有資料,且c為奇數,那麼進位,c變成(c + 1)
    3. 如果d後面還有非0數字,例如實際上小數為a.bcdef,此時一定要進位,c變成(c + 1)

關於奇進偶舍,有興趣的同學可以在維基百科搜尋這兩個詞條:數值修約奇進偶舍

所以,round給出的結果如果與你設想的不一樣,那麼你需要考慮兩個原因:

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

如何正確進行四捨五入

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

如何正確使用decimal模組呢?

看官方文件,不要看中文垃圾部落格!!!

看官方文件,不要看中文垃圾部落格!!!

看官方文件,不要看中文垃圾部落格!!!

不要擔心看不懂英文,Python已經推出了官方中文文件(有些函式的使用方法還沒有翻譯完成)。

我們來看一下:docs.python.org/zh-cn/3/lib…

官方文件給出了具體的寫法:

>>>Decimal('1.41421356').quantize(Decimal('1.000'))
Decimal('1.414')
複製程式碼

那麼我們來測試一下,0.1250.375分別保留兩位小數是多少:

>>> from decimal import Decimal
>>> Decimal('0.125').quantize(Decimal('0.00'))
Decimal('0.12')
>>> Decimal('0.375').quantize(Decimal('0.00'))
Decimal('0.38')
複製程式碼

怎麼結果和round一樣?我們來看看文件中quantize的函式原型和文件說明:

為什麼你需要少看垃圾部落格以及如何在Python裡精確地四捨五入

這裡提到了可以通過指定rounding引數來確定進位方式。如果沒有指定rounding引數,那麼預設使用上下文提供的進位方式。

現在我們來檢視一下預設上下文中的進位方式是什麼:

>>> from decimal import getcontext
>>> getcontext().rounding
'ROUND_HALF_EVEN'
複製程式碼

如下圖所示:

為什麼你需要少看垃圾部落格以及如何在Python裡精確地四捨五入

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

>>> from decimal import Decimal, ROUND_HALF_UP
>>> Decimal('0.375').quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
Decimal('0.38')
>>> Decimal('0.125').quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
Decimal('0.13')
複製程式碼

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

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

來實驗一下:


>>> Decimal(0.375).quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
Decimal('0.38')
>>> Decimal(0.125).quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
Decimal('0.13')
複製程式碼

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

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

>>> Decimal(11.245).quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
Decimal('11.24')
>>> Decimal('11.245').quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
Decimal('11.25')
複製程式碼

為什麼浮點數11.245和字串'11.245',傳進去以後,結果不一樣?

我們繼續在文件在尋找答案。

為什麼你需要少看垃圾部落格以及如何在Python裡精確地四捨五入

官方文件已經很清楚地說明了,如果你傳入的引數為浮點數,並且這個浮點值在計算機裡面不能被精確儲存,那麼它會先被轉換為一個不精確的二進位制值,然後再把這個不精確的二進位制值轉換為等效的十進位制值

對於不能精確表示的小數,當你傳入的時候,Python在拿到這個數前,這個數就已經被轉成了一個不精確的數了。所以你雖然引數傳入的是11.245,但是Python拿到的實際上是11.244999999999...

但是如果你傳入的是字串'11.245',那麼Python拿到它的時候,就能知道這是11.245,不會提前被轉換為一個不精確的值,所以,建議給Decimal的第一個引數傳入字串型的浮點數,而不是直接寫浮點數。

總結,如果想實現精確的四捨五入,程式碼應該這樣寫:

from decimal import Decimal, ROUND_HALF_UP

origin_num = Decimal('11.245')
answer_num = origin_num.quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
print(answer_num)
複製程式碼

執行效果如下圖所示:

為什麼你需要少看垃圾部落格以及如何在Python裡精確地四捨五入

特別注意,一旦要做精確計算,那麼就不應該再單獨使用浮點數,而是應該總是使用Decimal('浮點數')。否則,當你賦值的時候,精度已經被丟失了,建議全程使用Decimal舉例:

a = Decimal('0.1')
b = Decimal('0.2')
c = a + b
print(c)
複製程式碼

最後,如果有同學想知道為什麼0.125和0.375能被精確的儲存,而1.115、11.245不能被精確儲存,請在這篇文章下面留言,如果想知道的同學多,我就寫一篇文章來說明。

最後的最後,如果英文看不懂,中文文件也太枯燥,那麼就用掘金吧。掘金的高質量中文文章,比CSDN之流高出一萬倍。

如果文章對你有幫助,請考慮關注我的微信公眾號:未聞Code(ID:istkingname)

為什麼你需要少看垃圾部落格以及如何在Python裡精確地四捨五入

相關文章