round()
是 Python 內建的捨入法函式, 它的規格如下:
round(數值, 位數)
初次使用可能會不大習慣, 因為它採用的並不是四捨五入, 而是依照指定的位數, 往前或是往後取最接近的數, 例如:
>>> round(2.2251, 2)
2.23
因為取到 2 位小數, 所以最接近 2.2251 的數值是 2.23。
round()
的擇『偶』規則
你可能會想說這不就是四捨五入嗎?請看下一個範例:
>>> round(4.5, 0)
4.0
取小數 0 位, 等於取到個位數, 只保留整數, 如果是四捨五入, 就應該進位成 5.0 才對, 請再看下個範例:
>>> round(3.5, 0)
4.0
剛剛 5 不進位, 但是這個 5 卻進位?前面有提過 round()
是取最接近的數值, 可是當往前與往後的數值等距時, 會取偶數, 因此剛剛的範例都是取偶數的 4, 而不會取奇數的 5 或是 3。
這種捨入法稱為『偶數捨入法 (round half to even)』或是『銀行家捨入法 (banker's round)』, 也是 IEEE 754 標準裡的捨入法方式。它的用意是要解決多筆數字以四捨五入後加總平均會偏高的問題, 讓遇到中間值時捨位與進位的機率相等, 而非一律進位。這也是科學研究取有效數字時的偏好方式。
指定負的位數
round()
的第 2 個參數可以是負數, 0 是個位數、-1 是十位數、-2 是百位數、...以此類推, 例如:
>>> round(4351.3455, 0)
4351.0
>>> round(4351.3455, -1)
4350.0
>>> round(4351.3455, -2)
4400.0
>>> round(4351.3455, -3)
4000.0
round()
是看整個數值而非只看下一位數
有些教材會把 round()
解釋為『四捨六入, 遇到 5 看前一位是奇數才進位』, 不過這很可能導致誤解, 請看一開始我們舉的例子:
>>> round(2.2251, 2)
2.23
取到小數點第 2 位, 而小數第 3 位是 5, 前一位是 2, 如果只看小數第 3 位來決定是否進位, 依據『擇偶』規則, 前一位數是偶數, 應該不進位, 但實際的結果卻是進位成 2.23。這是因為 round()
並不是看小數第 3 位, 而是以 2.2251 來看, 2.23 要比 2.22 接近 2.2251, 因此結果為 2.23。
浮點數潛在的誤差
浮點數因為是以 2 進位的有限位元數來表示, 許多數值儲存的是近似值, 如果只顯示幾位小數分辨不出來, 但若是顯示多位小數, 就可以看出差別, 例如:
>>> "{:1.5f}".format(2.2250)
'2.22500'
>>> "{:1.20f}".format(2.2250)
'2.22500000000000008882'
你可以看到 2.2250 實際上儲存的值比 2.2250 大一點點, 如果用 round()
取到小數第 2 位就會發現怪怪的:
>>> round(2.2250, 2)
2.23
2.2250 明明與 2.23 及 2.22 等距, 依據『擇偶』規則, 應該選偶數的 2.22, 但結果是 2.23, 這就是因為實際的值比 2.2250 大, 離 2.23 比較近的關係。再來看一個例子:
>>> round(2.2350, 2)
2.23
依據『擇偶』規則, 應該選偶數的 2.24, 但結果卻是 2.23, 一樣把多位小數印出來見真章:
>>> "{:1.20f}".format(2.2350)
'2.23499999999999987566'
原來實際儲存的值比 2.2350 小一點點, 所以 2.23 比較接近, 而不是 2.24。
瞭解這一點, 對於某些神奇的運算結果就不會驚訝了, 如果對於浮點數有興趣, 可以參考 Python 官網上的這一篇文章。
使用 decimal 模組的 Decimal 類別
如果對於浮點數的誤差很介意, 那麼可以試試看使用 decimal
模組內的 Decimal 類別
, 這是專以 10 進位觀點設計的數值類別, 先來看一個範例:
>>> import decimal
>>> round(decimal.Decimal('2.2350'), 2)
Decimal('2.24')
這裡使用字串建立 Decimal
物件, 套用 round()
會看到 2.2350 依照擇偶規則進位成 2.24, 而不是之前範例的 2.23。
請注意 Decimal
物件也可以透過浮點數建立, 但是會原封不動保留誤差, 因此底下的範例仍會變成 2.23:
>>> round(decimal.Decimal(2.2350), 2)
Decimal('2.23')
變更捨入法
decimal
模組提供有 Context
類別的物件, 可透過 getcontext()
函式取得, 控制捨進位方式等等, 例如:
>>> c = decimal.getcontext()
>>> c.rounding = decimal.ROUND_UP
這樣會將捨進位方式改成無條件進位:
>>> round(decimal.Decimal(2.2350), 2)
Decimal('2.24')
>>> round(decimal.Decimal(2.2310), 2)
Decimal('2.24')
現在即使是 2.2310 也會進位成 2.24。
有關 Decimal
的各種捨進位方式, 可參考官方文件。
客製類別的捨入法
實際上 round()
倚賴的是個別類別的 __round__()
方法來協助捨進位, 因此您也可以為自訂類別設計專屬的捨進位方式, 例如:
>>> import math
>>> class MyFloat:
... def __init__(self, f):
... self.f = f
... def __round__(self, d=0):
... f = self.f * pow(10, d)
... f = float(math.ceil(f))
... return f * pow(10, -d)
...
>>>
在這個類別中, 就任性的採用無條件進位, 我們可以測試看看:
>>> round(MyFloat(2.13), 1)
2.2
>>> round(MyFloat(2.13), 0)
3.0
小結
本文說明 round()
內建函式的用法, 主要希望能提醒大家, 即使是這樣看似簡單的功能, 如果不注意細節, 都有可能會讓程式產生意料之外的結果, 務必要謹慎小心。
Top comments (0)