【Python】四捨五入にはDecimal.quantize()を使う

:::note info
使用Version：Python 3.11.2
:::

# 結論
Pythonで四捨五入する際にはround関数ではなく <font color="#ff3030"><b>Decimal.quantize()</b></font> を利用する
round関数は四捨五入するのではなく<b>偶数丸め（銀行丸め）</b>をする
```Python3:120.5 を一の桁まで四捨五入する場合
from decimal import *  # あるいは from decimal import Decimal, ROUND_HALF_UP

# 121 が返る
Decimal("120.5").quantize(Decimal("1"), rounding=ROUND_HALF_UP)
# 120 が返る
round(120.5)
```


## decimalモジュール
decimalモジュールは十進数を正確に表現するためのPython3標準ライブラリである
https://docs.python.org/ja/3/library/decimal.html

:::note info
decimalモジュールで四捨五入するための手順
1. Decimal型のオブジェクトを作る
   - `Decimal(<文字列>)`で宣言する
   - 数値型を渡すと正しい精度で計算できないため<font color="#ff3030">必ず文字列型を渡すこと</font>
2. quantizeメソッドで四捨五入する
   - `Decimal(<文字列>).quantize(有効桁、rounding=ROUND_HALF_UP)` 
      - 有効桁も Decimal型オブジェクト で表現する
      - ex. 一の桁までなら`Decimal("1")`
      - ex. 小数点第一位までなら`Decimal("0.1")`
:::

:::note info
丸めモードに`ROUND_HALF_UP`を指定することで四捨五入する
切り上げや切り捨て処理に変更することも可能
https://docs.python.org/ja/3/library/decimal.html#rounding-modes
:::

## round関数
偶数丸め（銀行丸め）は結果の最終桁が偶数になるように</b>丸め処理する

- <font color="#ff3030">最終桁が`5`のとき四捨五入の結果と異なる場合がある</font>
- つまり数によっては四捨五入の結果と偶数丸めの結果が一致することもある

### 【`0.1` から `1.0` を一の位まで丸める場合】

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194558/9b238ea8-74bc-d632-e928-2245d520a7a8.png)

- `0.5` のときについて
  - Decimal.quantize() は `1` を返している
    - 正しく四捨五入できている
  - round() は `0` を返している
    - `0` と `1` のうち偶数である `0` が選ばれる 

### 【`1.1` から `2.0` を一の位まで丸める場合】

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194558/558845f4-2d1b-8859-ec37-892b91d8a6a9.png)

- `1.5` のときについて
  - Decimal.quantize() は `2` を返している
    - 正しく四捨五入できている
  - round() も `2` を返している
    - `1` と `2` のうち偶数である `2` が選ばれる 
:::



## 実験
様々な関数を用いて丸め処理を実行する
```Python3:test.py
from decimal import *
import unittest

# 四捨五入する関数（有効桁ごとに用意）
my_round_a = lambda n:Decimal(str(n)).quantize(Decimal('1'), rounding=ROUND_HALF_UP)
my_round_b = lambda n:Decimal(str(n)).quantize(Decimal('0.1'), rounding=ROUND_HALF_UP)
my_round_c = lambda n:Decimal(str(n)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
my_round_d = lambda n:Decimal(str(n)).quantize(Decimal('0.001'), rounding=ROUND_HALF_UP)

# テストケースの作成
class testMyRound(unittest.TestCase):
    def setUp(self):
        pass

    def test_round_1(self):
        # 0.5
        self.assertEqual(round(0.5), 0)
        self.assertEqual(my_round_a(0.5), Decimal('1'))
        # 1.5
        self.assertEqual(round(1.5), 2)
        self.assertEqual(my_round_a(1.5), Decimal('2'))
        # 2.5
        self.assertEqual(round(2.5), 2)
        self.assertEqual(my_round_a(2.5), Decimal('3'))

    def test_round_2(self):
        # 1.5000を一の桁まで四捨五入
        self.assertEqual(round(1.5000), 2)
        self.assertEqual(round(1.5000, 0), 2.0)
        self.assertEqual(my_round_a(1.5000), Decimal('2'))
        # 1.4500を小数点第一位まで四捨五入
        self.assertEqual(round(1.4500, 1), 1.4)
        self.assertEqual(my_round_b(1.4500), Decimal('1.5'))
        # 1.5450を小数点第二位まで四捨五入
        self.assertEqual(round(1.5450, 2), 1.54)
        self.assertEqual(my_round_c(1.5450), Decimal('1.55'))
        # 1.5545を小数点第三位まで四捨五入
        self.assertEqual(round(1.5545, 3), 1.554)
        self.assertEqual(my_round_d(1.5545), Decimal('1.555'))

    def test_round_3(self):
        # 2.5000を一の桁まで四捨五入
        self.assertEqual(round(2.5000), 2)
        self.assertEqual(round(2.5000, 0), 2.0)
        self.assertEqual(my_round_a(2.5000), Decimal('3'))
        # 2.5500を小数点第一位まで四捨五入
        self.assertEqual(round(2.5500, 1), 2.5)
        self.assertEqual(my_round_b(2.5500), Decimal('2.6'))
        # 2.5550を小数点第二位まで四捨五入
        self.assertEqual(round(2.5550, 2), 2.56)
        self.assertEqual(my_round_c(2.5550), Decimal('2.56'))
        # 2.5555を小数点第三位まで四捨五入
        self.assertEqual(round(2.5555, 3), 2.555)
        self.assertEqual(my_round_d(2.5555), Decimal('2.556'))

    def tearDown(self):
        pass

if __name__ == "__main__":
    suite = unittest.TestSuite()
    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(testMyRound))
    unittest.TextTestRunner(verbosity=2).run(suite)
```
```:実行結果
test_round_1 (__main__.testMyRound) ... ok
test_round_2 (__main__.testMyRound) ... ok
test_round_3 (__main__.testMyRound) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK
```

Decimal.quantize()を使うことで正しく四捨五入できることが確認できる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194558/ee7c5ab2-fcd6-69c4-e491-7fd08f25eb65.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194558/e95f8650-18f6-78ae-3048-ed869e271cc6.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194558/7df27cbd-70ac-cef6-8265-48ae86a103b4.png)
:::

:::note info
テストケースtest_round_3ではround関数で正しく偶数丸めできていない
その原因はfloatが浮動小数点数を正確に表せないことにある
> https://docs.python.org/ja/3/library/functions.html?highlight=round#round
> 浮動小数点数に対する round() の振る舞いは意外なものかもしれません: 例えば、 round(2.675, 2) は予想通りの 2.68 ではなく 2.67 を与えます。これはバグではありません: これはほとんどの小数が浮動小数点数で正確に表せないことの結果です。
:::

## decimalモジュールを使わない方法
Python3の組み込み関数のみでも四捨五入することができる
（コメント欄にて情報いただきました。ありがとうございます。）
```Python3
# 書き方その1
"""
Args:
   x: 四捨五入対象の値
   n: 桁の指定(round関数と同様)
"""
round2 = lambda x,n=0:(1 if x >= 0 else -1) * int(abs(x) * 10**n + 0.5)/10**n

# 書き方その2
def round2(x,n=0):
   if x>=0:
      int(x * 10**n + 0.5)/10**n
   else
      int(x * 10**n - 0.5)/10**n
```
:::note info
負の数の四捨五入では一度絶対値について四捨五入してから符号を付ける
（JIS Z 8401規格にて定義されている）
1. 絶対値を取得する
2. 10の累乗倍する
3. 0.5を足す
4. int関数で端数を切り捨てする
5. 10の累乗倍で割る
6. 適切な符号を付ける
:::

## 参考記事
- https://qiita.com/ulxsth/items/67f0673adb9c86ecd542
- https://qiita.com/Kai_Koyama/items/58b221124354c4d59f69
- https://dev.classmethod.jp/articles/decimal-howto-method/