DEV Community

codemee
codemee

Posted on • Edited on

Python 的 round() 與 decimal 模組

round() 是 Python 內建的捨入法函式, 它的規格如下:

round(數值, 位數)
Enter fullscreen mode Exit fullscreen mode

初次使用可能會不大習慣, 因為它採用的並不是四捨五入, 而是依照指定的位數, 往前或是往後取最接近的數, 例如:

>>> round(2.2251, 2)
2.23
Enter fullscreen mode Exit fullscreen mode

因為取到 2 位小數, 所以最接近 2.2251 的數值是 2.23。

round() 的擇『偶』規則

你可能會想說這不就是四捨五入嗎?請看下一個範例:

>>> round(4.5, 0)
4.0
Enter fullscreen mode Exit fullscreen mode

取小數 0 位, 等於取到個位數, 只保留整數, 如果是四捨五入, 就應該進位成 5.0 才對, 請再看下個範例:

>>> round(3.5, 0)
4.0
Enter fullscreen mode Exit fullscreen mode

剛剛 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
Enter fullscreen mode Exit fullscreen mode

round() 是看整個數值而非只看下一位數

有些教材會把 round() 解釋為『四捨六入, 遇到 5 看前一位是奇數才進位』, 不過這很可能導致誤解, 請看一開始我們舉的例子:

>>> round(2.2251, 2)
2.23
Enter fullscreen mode Exit fullscreen mode

取到小數點第 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'
Enter fullscreen mode Exit fullscreen mode

你可以看到 2.2250 實際上儲存的值比 2.2250 大一點點, 如果用 round() 取到小數第 2 位就會發現怪怪的:

>>> round(2.2250, 2)
2.23
Enter fullscreen mode Exit fullscreen mode

2.2250 明明與 2.23 及 2.22 等距, 依據『擇偶』規則, 應該選偶數的 2.22, 但結果是 2.23, 這就是因為實際的值比 2.2250 大, 離 2.23 比較近的關係。再來看一個例子:

>>> round(2.2350, 2)
2.23
Enter fullscreen mode Exit fullscreen mode

依據『擇偶』規則, 應該選偶數的 2.24, 但結果卻是 2.23, 一樣把多位小數印出來見真章:

>>> "{:1.20f}".format(2.2350)
'2.23499999999999987566'
Enter fullscreen mode Exit fullscreen mode

原來實際儲存的值比 2.2350 小一點點, 所以 2.23 比較接近, 而不是 2.24。

瞭解這一點, 對於某些神奇的運算結果就不會驚訝了, 如果對於浮點數有興趣, 可以參考 Python 官網上的這一篇文章

使用 decimal 模組的 Decimal 類別

如果對於浮點數的誤差很介意, 那麼可以試試看使用 decimal 模組內的 Decimal 類別, 這是專以 10 進位觀點設計的數值類別, 先來看一個範例:

>>> import decimal
>>> round(decimal.Decimal('2.2350'), 2)
Decimal('2.24')
Enter fullscreen mode Exit fullscreen mode

這裡使用字串建立 Decimal 物件, 套用 round() 會看到 2.2350 依照擇偶規則進位成 2.24, 而不是之前範例的 2.23。

請注意 Decimal 物件也可以透過浮點數建立, 但是會原封不動保留誤差, 因此底下的範例仍會變成 2.23:

>>> round(decimal.Decimal(2.2350), 2)
Decimal('2.23')
Enter fullscreen mode Exit fullscreen mode

變更捨入法

decimal 模組提供有 Context 類別的物件, 可透過 getcontext() 函式取得, 控制捨進位方式等等, 例如:

>>> c = decimal.getcontext()
>>> c.rounding = decimal.ROUND_UP
Enter fullscreen mode Exit fullscreen mode

這樣會將捨進位方式改成無條件進位:

>>> round(decimal.Decimal(2.2350), 2)
Decimal('2.24')
>>> round(decimal.Decimal(2.2310), 2)
Decimal('2.24')
Enter fullscreen mode Exit fullscreen mode

現在即使是 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)
...
>>>
Enter fullscreen mode Exit fullscreen mode

在這個類別中, 就任性的採用無條件進位, 我們可以測試看看:

>>> round(MyFloat(2.13), 1)
2.2
>>> round(MyFloat(2.13), 0)
3.0
Enter fullscreen mode Exit fullscreen mode

小結

本文說明 round() 內建函式的用法, 主要希望能提醒大家, 即使是這樣看似簡單的功能, 如果不注意細節, 都有可能會讓程式產生意料之外的結果, 務必要謹慎小心。

Top comments (0)