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