PythonでのDecimalで「文字列化」「精度維持」等のよくある「こうしたい」を試してみた

PythonでDecimalを使った高精度な計算にて、延々と悩ませられたことをまとめてみました。
2019.07.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

金融や会計等の高精度をする場合にPythonではDecimalの利用が好ましいのですが、用途故に利用頻度もそこまで高くなく、実際に使ってみると慣れないうちはモジュールの動作に振り回されるような感覚に陥ります。

約一ヶ月程度Decimalに振り回されつつ、どのように扱えばいいのか判ってきた範囲でまとめてみました。

Decimalを使うべきシーン

浮動小数点を交える数値及び計算が対象です。float型では正確に計算できないケースにも対応できます。

>>> float('0.3') - float('0.2')
0.09999999999999998
>>> from decimal import Decimal
>>> Decimal('0.3') - Decimal('0.2')
Decimal('0.1')

Decimalに渡す値は文字列にすべきという点だけ気をつけましょう。float型で渡した場合、すでに精度が低い状態となっているために計算結果が不正確となります

>>> from decimal import Decimal
>>> Decimal(0.3) - Decimal(0.2)
Decimal('0.09999999999999997779553950750')

精度の混在を避ける

Decimalにしたとして、float型のデータと計算していた場合は精度が担保できません。誤った計算を防ぎたい場合はgetcontextを用いることでFloatOperation経由で例外として対処することも可能です。

>>> from decimal import Decimal, getcontext, FloatOperation
>>> getcontext().traps[FloatOperation] = True
>>> Decimal(3.14)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
decimal.FloatOperation: [<class 'decimal.FloatOperation'>]
>>> Decimal('3.14')
Decimal('3.14')
>>> Decimal('3.14') < 3.14
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
decimal.FloatOperation: [<class 'decimal.FloatOperation'>]

文字列型として出力する

Decimalとして扱ったデータをそのまま出力する場合、小数点以下の桁数が一定以上になると指数にて短縮表記されます。短縮せずに全ての桁を表示させたい場合はformatにて行います。

>>> pi = Decimal('3.1415926435')
>>> '{:.10f}'.format(pi)
'3.1415926435'

状況に応じての指定桁数での丸めも併用する場合

状況に応じて変動する指定桁数がありつつformatで文字列として出力したい場合には、quantizeを併用しつつ以下のような手段もあります。

丸め方 rounding
四捨五入 ROUND_HALF_UP
切り捨て ROUND_DOWN
切り上げ ROUND_UP
>>> import re
>>> from decimal import Decimal, ROUND_DOWN
>>> digit = '0.00000000001'
>>> result = re.match(r'\d*.(?P<digit>\d+)$', digit)
>>> strformat = '{' + ':.{digit}f'.format(digit=len(result.group('digit'))) + '}'}'
>>> strformat
'{:.11f}'
>>> strformat.format(Decimal('1.112345678911').quantize(Decimal(digit), rounding=ROUND_DOWN))
'1.11234567891'

計算後の丸め方を固定化する

個別指定せずにgetcontextからまとめて指定しておく方法もあります。注意すべき点として、Decimal型にしただけでは反映されません

>>> from decimal import getcontext, Decimal
>>> getcontext().prec = 6
>>> getcontext().rounding = ROUND_DOWN
>>> Decimal('1.1111111121')
Decimal('1.1111111121')
>>> Decimal('1.1111111121') + Decimal('1.1111111121')
Decimal('2.22222')

まとめ

Decimalについては公式ドキュメントに詳細が掲載されていますが、
実務にてやりたいことを実現する方法≠ドキュメント手順
という状態はよくあります。

decimal --- 十進固定及び浮動小数点数の算術演算 — Python 3.7.4 ドキュメント

半ば、私自身が扱い方を忘れた時に備えた備忘録エントリーですが、Decimalの使いかたの為に延々と頭を抱えながら時間を掛けて調べる手間を省けることにつながれば幸いです。