2022年2月18日 星期五

Python 學習筆記 : 內建函式 round() 是四捨五入還是五捨六入?

今天在讀 "Python 入門-邁向高手之路 (深智, 2018)" 時看到 round() 函式的範例, 作者將此函式的功能稱為五捨六入, 我一直認為 round() 是四捨五入, 但其實有時候是四捨五入, 有時候又是五捨六入, 如果在數值計算上有此誤解, 可能會積小差異變成大差異而得到錯誤的結果, 例如 : 

>>> round(0.4)        # 4 捨
0
>>> round(0.5)        # 5 捨
0
>>> round(0.6)        # 6 入
1

由此例可見 round() 確實不是四捨五入, 但下面範例卻打臉五捨六入的說法 : 

>>> round(0.47)     # 4 捨  
0
>>> round(0.57)     # 5入
1
>>> round(0.67)     # 6 入
1

此例顯示有些時候是四捨五入, 有些時候是五捨六入, 到底是甚麼原因呢? 其實這是因為電腦是二進制數值系統, 它可以精確地儲存整數, 但無法精確地儲存浮點數, 它儲存的其實只是近似值而已, 就是這個近似值的誤差使得 round() 有時四捨五入, 有時五捨六入的原因, 參考 :


用 print() 格式化字串來輸出變數實際儲存在記憶體中的數值即可明白這小小的誤差 : 

>>> print('%.50d' % 12345)       # 整數可精確地在二進位系統中儲存
00000000000000000000000000000000000000000000012345
>>> print('%.50f' % 0.4)             # 浮點數無法精確地在二進位系統中儲存
0.40000000000000002220446049250313080847263336181641
>>> print('%.50f' % 0.5)    
0.50000000000000000000000000000000000000000000000000
>>> print('%.50f' % 0.6)       
0.59999999999999997779553950749686919152736663818359
>>> print('%.50f' % 0.47)      
0.46999999999999997335464740899624302983283996582031
>>> print('%.50f' % 0.57)      
0.56999999999999995115018691649311222136020660400391
>>> print('%.50f' % 0.67)     

可見在二進制系統中, 浮點數儲存在記憶體中並非那麼精確, 與我們所認為的值有些許誤差, 不像整數可精確地儲存. 關於 print() 格式化字串參考 : 


Python 3 的內建函式 round() 可以傳入第二參數指定精確度, 語法如下 :

round( x [, n] ) 

其中備選參數 n 用來指定傳回值的小數位數. 

例如 : 

>>> round(3.15, 1)     # 五捨六入
3.1
>>> round(3.16, 1)     # 五捨六入
3.2
>>> round(4.14, 1)     # 四捨五入 (接近 4.1)
4.1
>>> round(4.15, 1)     # 四捨五入 
4.2
>>> round(4.16, 1)     # 四捨五入
4.2
>>> round(3.775, 2)   # 五捨六入
3.77
>>> round(3.776, 2)   # 五捨六入
3.78

可見 round() 有時四捨五入, 有時五捨六入, 下面這篇文章說其實是有規律的 :


其規則如下 : 
  1. 如果沒有傳入第二參數指定小數位數 (即傳回整數), 則 round() 會傳回最接近的整數.
  2. 如果有傳入第二參數指定小數位數, 則根據第二參數位數的奇偶性來決定是要入還是捨, 若第二參數位數為偶數則捨, 第二參數位數為偶數則入. 
第一項是說若未傳入第二參數 n 則 round(x) 會傳回最靠近 x 的整數, 但所謂 "最靠近" 卻沒提遇到 5 是捨是入的規則, 例如 : 

>>> round(0.4)       # 四捨
0
>>> round(0.5)       # 五捨
0
>>> round(0.50)     # 五捨
0
>>> round(0.51)     # 五入
1
>>> round(0.501)   # 五入
1
>>> round(1.4)       # 四捨
1
>>> round(1.5)       # 五入
2
>>> round(123.5)    # 五捨
124

可見小數後一位是 5 時到底要不要進位無法預測. 至於有傳入第二參數時要依據該位數的奇偶來判定是否進位採取奇捨偶入, 但下面卻是反例 :

>>> round(3.15, 1)        # 1 是奇數要捨 : 猜對
3.1
>>> round(3.25, 1)        # 2 是偶數要入 : 正確
3.2
>>> round(3.35, 1)        # 3 是奇數要捨 : 猜錯
3.4
>>> round(3.45, 1)        # 4 是偶數要入 : 正確
3.5

所以該篇所說的規則其實不完備. 與其猜 round() 是四捨五入還是五捨六入, 不如問要如何得到數學上的四捨五入更重要. 其實指定四捨五入位數的情形可用 Numpy 的 round() 來解決, 例如 : 

>>> import numpy as np     
>>> round(2.675, 2)             
2.67   
>>> np.round(2.675, 2)    
2.68
>>> np.round(3.35, 1)    
3.4
>>> np.round(4.15, 1)   
4.2
>>> np.round(3.775, 2)   
3.78

可見 Numpy 對於指定位數都符合四捨五入規則, 但要注意的是 np.round() 傳回的是 Numpy 的 float 型態, 可以用 float() 將其轉回 Python 原生的 float 型態 :

>>> type(np.round(4.15, 1))     
<class 'numpy.float64'>   
>>> float(np.round(4.15, 1))     
4.2

但若呼叫 np.round() 時不指定位數的話四捨五入就不靈了, 例如 : 

>>> np.round(0.5)            
0.0
>>> int(np.round(0.5))     
0
>>> np.round(0.501)  
1.0
>>> np.round(0.5000001)     
1.0
>>> np.round(0.6)   
1.0
>>> int(np.round(0.6))   
1

可見若小數點後只有 5 會被捨去, 但若比 5 多一點點就會進位了. 

參考 : 



2022-02-18 補充 :

我剛剛在下面這篇文章中找到 Numpy 沒有正確進行四捨五入的範例 :


最上面編號 107 的回應裡使用 655.665 取小數兩位, 四捨五入應該是 655.67 才對, 但 Numpy 卻傳回 655.66, 捨去沒有進位 : 

>>> np.round(655.665, 2)       
655.66

看來 Numpy 似乎也不太牢靠. 

不過編號 36 的回應卻給出期望的 655.67, 它使用 decimal.Decimal 類別搭配 ROUND_UP 常數來做, 所以首先要從內建模組 decimal 中匯入這兩個 : 

>>> from decimal import Decimal, ROUND_UP     
>>> Decimal(str(655.665)).quantize(Decimal('.01'), rounding=ROUND_UP)     
Decimal('655.67')

可見傳回值是一個 Decimal 物件, 可用 float() 轉成浮點數物件 : 

>>> float(Decimal(str(655.665)).quantize(Decimal('.01'), rounding=ROUND_UP))    
655.67

上面的範例改用這方法都正確輸出四捨五入結果, 例如 :

>>> float(Decimal(str(2.675)).quantize(Decimal('.01'), rounding=ROUND_UP))
2.68
>>> float(Decimal(str(3.35)).quantize(Decimal('.1'), rounding=ROUND_UP))    
3.4
>>> float(Decimal(str(4.15)).quantize(Decimal('.1'), rounding=ROUND_UP))
4.2
>>> float(Decimal(str(4.775)).quantize(Decimal('.01'), rounding=ROUND_UP))
4.78

沒有留言 :