【Python】「四捨五入」實作

無論在數學上,或是生活運用都很常見到四捨五入,就好像打折或是計算服務費,當有小數點時,就會想要用四捨五入來取到整數。

而在Python 是怎樣使用呢?該不該使用round 呢?還有在計算時有什麼細節呢?


Python版本:

  • 3.8.5
  • 3.9.2

 

Round

Python 中內建有提供一個round的功能(build-in function),用來進行四捨五入的計算,在網路上也很多教學網站也是使用這個。但這個有一些小問題,讓我們探究竟。

 

round:整數

首先定義好個數字,分別是0.51.5,然後用round功能,取得小數點第一位,也就是直接用round(數字),不用輸入第二個參數。

case_a = 0.75
round_case_a = round(case_a, 1)
print(f"case_a: {case_a}, round: {round_case_a}")

case_b = 0.85
round_case_b = round(case_b, 1)
print(f"case_b: {case_b}, round: {round_case_b}")

但是看到結果分別是:case_a: 0.75, round: 0.8case_b: 0.85, round: 0.8,看起來怪怪的,0.75 四捨五入後是0.8 沒錯,但而0.85 四捨五入後是也是0.8,到底是怎麼回事?

官方提供的文件是這樣說

Return number rounded to ndigits precision after the decimal point. If ndigits is omitted or is  None, it returns the nearest integer to its input.

到底什麼是最近的數字呢?文件又繼續說到:

For the built-in types supporting round(), values are rounded to the closest multiple of 10 to the power minus ndigits; if two multiples are equally close, rounding is done toward the even choice(so, for example, both  round(0.5) and round(-0.5) are 0 , and round(1.5) is 2).

所以我們可以知道,使用「round」他會找到最近的整數,這並不是bug(官方說的,不是我!)。這其實是每個程式語言都會遇到的問題,這造成早期程式計算中有很多溢位相關的問題,但這不在話下。參考官方文件,可以知道在進行小數點的時候,會存在一個極限,導致計算會失準。因為先天的限制,所以Python 在round會有個特性(直接說答案XD),也就是Python 會在整數時候自己去判斷進位到最近的偶數。

廢話不多說,直接看例子:

case_5 = 35
case_6 = 45
print(f"case_5: {case_5}, round: {round(case_5, -1)}") # round: 40
print(f"case_6: {case_6}, round: {round(case_6, -1)}") # round: 40

case_7 = 75
case_8 = 85
print(f"case_7: {case_7}, round: {round(case_7, -1)}") # round: 80
print(f"case_8: {case_8}, round: {round(case_8, -1)}") # round: 80

case_9 = 150
case_10 = 250
print(f"case_9: {case_9}, round: {round(case_9, -2)}") # round: 200
print(f"case_10: {case_10}, round: {round(case_10, -2)}") # round: 200

結果都是往偶數攏:

case_5: 35, round: 40
case_6: 45, round: 40
case_7: 75, round: 80
case_8: 85, round: 80
case_9: 150, round: 200
case_10: 250, round: 200

這跟我們認知的四捨五入真的不同耶!
那小數點的狀況如何呢?

 

round:小數點

浮點數看會不會也跟整數一樣,往偶數靠攏:

case_f_1 = 0.75
round_case_1 = round(case_f_1, 1)
print(f"case_f_1: {case_f_1}, round: {round_case_1}") # round: 0.8

case_f_2 = 0.85
round_case_2 = round(case_f_2, 1)
print(f"case_f_2: {case_f_2}, round: {round_case_2}") # round: 0.8

以範例來說,0.750.85四捨五入後應該都會是0.8,結果是沒錯。
再看看更多數字,結果發現不如預期:

case_f_3 = 0.075
case_f_4 = 0.085
print(f"case_f_3: {case_f_3}, round: {round(case_f_3, 2)}") # round: 0.07
print(f"case_f_4: {case_f_4}, round: {round(case_f_4, 2)}") # round: 0.09

0.0750.085在四捨五入後,分別是0.070.09,一個進位一個不進位,而且也不是向整數靠齊,到底為什麼呢?
這跟上面說的,在有小數的處理中程式都不是完美的,用format(case_f_3, '.20f')顯示0.75實際上是:0.07499999999999999722,而0.85則是0.08500000000000000611,所以0.75才不會進位。

小數點真的是另外的世界。
所以需要用另外的方法來進行四捨五入。


Decimal

可使用decimal模組,就可以精準的轉換。需要先import 套件進來:

from decimal import Decimal, ROUND_HALF_UP

首先要把我們的數字變成字串,然後在用quantize來轉換:

str_decimal = str(0.055)
case_d_1 = Decimal(str_decimal).quantize(Decimal(".00"), ROUND_HALF_UP)
print(f"case decimal 1: {case_d_1}")	# case decimal 1: 0.06

str_decimal_2 = str(0.065)
case_d_2 = Decimal(str_decimal_2).quantize(Decimal(".00"), ROUND_HALF_UP)
print(f"case decimal 2: {case_d_2}")	# case decimal 2: 0.07

str_decimal_3 = str(0.075)
case_d_3 = Decimal(str_decimal_3).quantize(Decimal(".00"), ROUND_HALF_UP)
print(f"case decimal 3: {case_d_3}")	# case decimal 3: 0.08

str_decimal_4 = str(0.085)
case_d_4 = Decimal(str_decimal_4).quantize(Decimal(".00"), ROUND_HALF_UP)
print(f"case decimal 4: {case_d_4}")	# case decimal 4: 0.09

四捨五入的結果符合我們的預期。

 

另外作法

網路上也有其他作法,就是要把小數轉成整數,然後再用「無條件進位」(math.cell())或是「無條件捨去」(math.floor())來達到四捨五入之目的,接著再除回去。但不推薦此作法,因為需要自行判斷是要使用無條件進位還是無條件捨去,這應該是由程式判斷,因此只紀錄有這方法,就不推薦了。


參考資料

 

~Copyright by Eyelash500~

IT技術文章EY*研究院
iT邦幫忙eyelash*睫毛
Blog睫毛*Relax
Facebook睫毛*Relax