小数を正確に計算する — decimalモジュール

コンピュータで浮動小数点型を扱うときに誤差の発生は避けられません。これはPythonも例外ではありません。Python組み込みの浮動小数点数型(float型)も、内部で2進数を用いて有限のビット数で数を表現しているからです。

float型には表現できる値の範囲(最小値と最大値の間)があり、それより小さいあるいは大きい値は表現できません。最小値と最大値の間でも、それらすべて数をfloat型で正確に表現できるわけではありません。0と1の間にも数は無限に存在することを考えれば、これは理解できるでしょう。float型は最小値と最大値の間の実数のうち、正確に表現できる数が飛び飛びで存在していますが、それ以外の数は近い値に丸められます。つまり、多くの場合は近似値です。さらにfloat型の演算でも種々の誤差が発生します。

整数型(int型)の演算で誤差は発生しません。

試しにPythonの対話型インタープリタで以下のような演算を行なってみてください。

>>> 0.1 + 0.2
0.30000000000000004

誤差はごくわずかなので、多くの場合は許容できるかもしれません。たとえば科学技術などの世界では、ある程度の精度が保てれば問題にならないことも多いです。しかし、金融の世界では状況は異なるでしょう。金額に少しでも誤差があれば信用問題になってしまいます。

このようなときのためにPythonではdecimalモジュールが用意されています。decimalモジュールを使うと先ほどの演算も正確に行うことができます。

>>> Decimal('0.1') + Decimal('0.2')
Decimal('0.3')

decimalモジュールは次の重要な機能を提供します。

  • (小数を含む)数値の正確に表現するDecimalクラスの提供
  • (小数を含む)数値の正確な演算
  • 誤差の発生や許されない演算?を実行した場合に検知できる。
  • 丸めの方法を完全に制御できる

この記事では小数を含む数値を正確に演算するためのdecimalモジュールについて解説していきます。

目次

Decimalオブジェクト

decimalモジュールには10進数で(小数を含む)数を正確に表現するDecimalクラスが用意されています。小数の正確な演算はDecimalクラスのインスタンスを使って行います。

なお、この後に説明するコードを実行するには、次のようにdecimalモジュールのすべての名前をインポートしてください。

>>> from decimal import *

Decimalオブジェクトの作成

Decimalクラスのインスタンスは、整数、文字列、浮動小数点数、タプル、または他のDecimalオブジェクトから作成することができます。Decimalオブジェクトはイミュータブル(変更不可能)です。

>>> Decimal(123)
Decimal('123')
>>> Decimal('1.23')
Decimal('1.23')
>>> Decimal(1.23)
Decimal('1.229999999999999982236431605997495353221893310546875')

float型から作成した場合、float型が(内部で)表す値と正確に等価なDecimalオブジェクトが作成されます。float型を使うと見た目とは異なる値のDecimalオブジェクトが作成されるので、float型の使用はお薦めしません。

新しく作成されるDecimalオブジェクトの有効桁数については次のような規則があります。

新たに作成されたDecimalオブジェクトの有効桁数は入力桁数だけで決まる。算術コンテキストの精度は演算結果の値にのみ影響を与え、作成されるDecimalオブジェクトの有効桁数に影響することはない。

Decimalオブジェクト作成のときに引数に渡される入力値は正確な値である見做されます。そのため入力値を忠実に表現するよう入力値と同じ桁数でDecimalオブジェクトは作成されます。

算術コンテキストについては後述しますが、Decimalオブジェクトの算術演算では、この算術コンテキストの精度が演算結果の値の有効桁数に影響を与えます。しかし、この精度はDecimalオブジェクト作成時の桁数には何ら影響を与えません。

これを理解するために次の例をみてください。これは次のことを実行しています。

  • 算術コンテキストの精度を3桁に設定(getcontext().prec = 3)
  • 新たなDecimalオブジェクトの生成。入力値は9桁(’1.23456789’)
  • Decimalオブジェクトに対して符号を反転する演算を実施
>>> getcontext().prec = 3
>>> d = Decimal('1.23456789')
>>> d
Decimal('1.23456789')
>>> -d
Decimal('-1.23')

新たに作成されるDecimalオブジェクトの有効桁数は引数(入力)の桁数と同じです。Decimalオブジェクトの作成前に算術コンテキストの精度を設定していますが、この設定は新たに作成されるDecimalオブジェクトの有効桁数に一切影響を与えません。一方、符号を反転する演算を実行すると、精度の桁数になるよう丸めが起こります。

Decimalオブジェクトは非数(NaN)、正負の無限大(Infinity)を表現することもできます。

>>> Decimal('NaN')
Decimal('NaN')
>>> Decimal('Infinity')
Decimal('Infinity')
>>> Decimal('-Infinity')
Decimal('-Infinity')

Decimalオブジェクトの演算

Decimalオブジェクトはintやfloatと同様に算術演算子で演算ができます。

>>> Decimal(-7) + Decimal(4)
Decimal('-3')
>>> Decimal(-7) - Decimal(4)
Decimal('-11')
>>> Decimal(-7) * Decimal(4)
Decimal('-28')
>>> Decimal(-7) / Decimal(4)
Decimal('-1.75')
>>> Decimal(-7) // Decimal(4)
Decimal('-1')
>>> Decimal(-7) % Decimal(4)
Decimal('-3')

intやfloatの演算と異なる部分も少しあります。

整数除算はintやfloatとの場合と異なり、商の切り捨てではなく0方向に丸めた整数部分を返します。

>>> (-7) // 4
-2
>>> Decimal(-7) // Decimal(4)
Decimal('-1')

剰余では、結果の符号は除数の符号ではなく被除数の符号と一致します。

>>> (-7) % 4
1
>>> Decimal(-7) % Decimal(4)
Decimal('-3')

int型との算術演算も可能です。

>>> Decimal(7) + 4
Decimal('11')
>>> Decimal(7) / 4
Decimal('1.75')

比較演算子によるnt型との比較も可能です。

>>> Decimal(-7) < Decimal(4)
True
>>> Decimal(-7) > Decimal(4)
False
>>> Decimal(-7) == Decimal(4)
False
>>> Decimal(-7) < 4
True
>>> Decimal(-7) > 4
False
>>> Decimal(-7) == 4
False

float型との算術演算はサポートされていません。実行しようとするとTypeError例外を送出します。ただし比較演算は可能です。

>>> Decimal(7) + 3.14
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'decimal.Decimal' and 'float'
>>> Decimal(7) > 3.14
True

演算結果の有効桁数

新たに作成されたDecimalオブジェクト有効桁数は入力の値だけで決まることは前述しました。演算結果の桁数では次のことを覚えておきましょう。ただし、これは演算結果の桁数がが算術コンテキストの精度を超えない場合です。精度を超える場合には丸めが発生するのでこの限りではありません。

decimalモジュールでは、有効桁数を示すために演算結果の末尾のゼロは残される。

たとえば以下のような加算の場合、「4.50」の末尾のゼロも含めて忠実に演算を実行すると結果は「16.80」です。そのため、演算結果の値も末尾ゼロは有効桁数を示すために残されます。

>>> Decimal('12.3') + Decimal('4.50')
Decimal('16.80')

乗算の場合も「4.50」の末尾のゼロも含めて忠実に演算すると結果の値は「55.350」です。そのため演算結果のすべての桁が有効桁数として残されます。

>>> Decimal('12.3') * Decimal('4.50')
Decimal('55.350')

算術コンテキスト

これまでにも算術コンテキストという言葉に少し触れましたが、ここで詳細を説明しましょう。

算術コンテキストとは、Decimalオブジェクトの算術演算を実行するときの環境設定です。算術コンテキストはContextオブジェクトで表されます。

算術コンテキストとは、算術演算における環境設定です。算術演算の精度や値丸めなどは、算術コンテキストの設定により制御されます。また算術コンテキストには、算術演算により値の丸めなど例外的な状況が発生したかどうかが記録されますので、演算後にそれを調べ対処することもできます。さらに、そのような状況が発生したときに例外を送出するよう設定することもできます。

現在の算術コンテキストはgetcontext()関数で取得することができます。

>>> getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])

表示されているのはContextオブジェクトの属性とその設定値です。これらの属性の値により、Decimalオブジェクトの算術演算を実行したときの振る舞いが決定されます。

Contextオブジェクの属性

Contextオブジェクの属性とその説明を次に示します。

Contextの属性説明デフォルト値
prec算術演算を行うときの計算精度。 整数(1〜MAX_PRECの範囲)で設定する。28
rounding丸め方法。decimalモジュールで定義されている属性のいずれかを設定する。ROUND_HALF_EVEN
flags演算により例外的な状態が発生したかを示すフラグ。フラグの設定なし
traps演算により例外的な状態が発生した場合、例外を送出するかを示すフラグ。InvalidOperation、DivisionByZero、 Overflowが発生したときに例外を送出
EminDecimalオブジェクトの指数の下限値。整数(MIN_EMIN〜0の範囲)で設定する。999999
EmaxDecimalオブジェクトの指数の上限値。整数(0〜MAX_EMINの範囲)で設定する。999999
capitals指数記号を大文字/小文字のいずれで表示するかを指示する。1(デフォルト)または0を設定する。1の場合は大文字(E)で出力し、0の場合は小文字(e)で出力する。1
clampDecimalオブジェクトの指数をclampするかを制御する。0(デフォルト)または1を設定する。0の場合はDecimalオブジェクトの調整後の指数は最大でEmaxになる。1の場合はDecimalオブジェクトの指数は「Emin – prec + 1 <= e <= Emax – prec + 1」の範囲に制限される。指数は可能な限りこの制限に沿うように減少され、係数にはゼロが追加される。これは数値を保存しますが、有効な末尾の 0 に関する情報を失います。0

Decimalオブジェクトは符号、係数部、指数を持っています。EminとEmaxはこの指数の下限値と上限値を設定します。

clampとは数値をある範囲(たとえば最小値と最大値との間)の間に制限することです。clampされると範囲より小さい値は範囲の最小値と同じになり、範囲より大きい値は範囲の最大値と同じ値になります。たとえば値の範囲が-10から10までのとき、-20という値は-10となり、20という値は10という値になります。

prec、rounding、flags、traps属性については、この後詳しく説明します。

精度と丸め

Decimalオブジェクトの演算では「演算結果の値」の桁数が精度(の桁数)を超えると丸めが発生します。演算結果の値が精度の範囲内であれば、丸めは発生せず正確な演算結果を得ることができます。

精度の桁数および丸め方法は、Contextオブジェクのprec属性およびrounding属性に設定します。

>>> getcontext().prec = 3
>>> getcontext().rounding = ROUND_CEILING

丸めは演算結果の値に対して行われることに注意してください。非演算子が演算の前に丸められることはありません。2番目の例では初めに+単項演算子の演算が実行されるため、その時点の得算結果の値に対して丸めが起こります。その後にその値が2倍されます。

>>> Decimal('3.141') * 2
Decimal('6.29')
>>> +Decimal('3.141') * 2
Decimal('6.30')

上述の2番目の演算のように、丸めは演算のたびに起こります。1つ目の演算では切り上げは1度しか発生しませんが、2番目の演算では切り上げが2度発生しています。

次の例では演算の順序により結果が異なります。

>>> Decimal('3.141') + Decimal('1.001') + Decimal('0.000')
Decimal('4.15')
>>> Decimal('3.141') + (Decimal('1.001') + Decimal('0.000'))
Decimal('4.16')

丸めの方法はContextオブジェクのrounding属性に設定します。設定できる値はdecimalモジュールの属性として定義されています。設定可能な値の一覧を以下に示します。

rounding属性に設定する値意味例(精度は2桁):
演算結果→丸め後の値
decimal.ROUND_CEILINGInfinity 方向に丸める。1.15 → 1.2
-1.15 → -1.1
decimal.ROUND_FLOOR-Infinity 方向に丸める。1.15 → 1.1
-1.15 → -1.2
decimal.ROUND_DOWNゼロ方向に丸める。1.15 → 1.1
-1.15 → -1.1
decimal.ROUND_UPゼロから遠い方向に丸める。1.15 → 1.2
-1.15 → -1.2
decimal.ROUND_HALF_DOWN近い方に丸める。中間はゼロ方向に丸める。1.14 → 1.1
1.15 → 1.2
-1.15 → -1.1
-1.16 → -1.2
decimal.ROUND_HALF_EVEN近い方に丸める。中間は偶数整数方向に丸める。1.05 → 1.0
-1.05 → -1.0
1.14 → 1.1
1.15 → 1.2
-1.14 → -1.1
-1.15 → -1.2
decimal.ROUND_HALF_UP近い方に丸める。中間はゼロから遠い方向に丸める。1.14 → 1.1
1.15 → 1.2
-1.14 → -1.1
-1.15 → -1.2
decimal.ROUND_05UPゼロ方向に丸めた後の最後の桁が 0 または 5 ならばゼロから遠い方向に、そうでなければゼロ方向に丸めます。1.01 → 1.1
1.41 → 1.4
1.51 → 1.6
1.91 → 1.9
-1.01 → -1.1
-1.41 → -1.4
-1.51 → -1.6
-1.91 → -1.9

シグナル

decimalモジュールには、演算中に発生する様々な例外的な状態を示すシグナルという機構があります。演算で例外的な状態が発生すると、Contextオブジェクトのシグナルに対応するフラグがセットされます。演算後にそのフラグを調べることにより、例外的な状態が発生したかチェックすることができます。たとえば計算結果が厳密な値か、あるいは丸めが発生していたかなど知ることができます。

また、例外的な状態が発生したときに例外を送出するようにすることもできます。

フラグを調べる

フラグはContextオブジェクトのflags属性にセットされます。

次の例は計算精度が3桁なので、単項演算子+による演算で値の丸めが発生しています。演算後にフラグの状態を調べると2つのフラグがセット(InexactとRoundedがTrue)されています。これは演算結果の値が丸められ、かつその値が厳密な計算結果ではないことを示しています。

>>> c = getcontext()
>>> c.prec = 3
>>> d = Decimal('5.001')
>>> c.flags
{<class 'decimal.InvalidOperation'>:False, <class 'decimal.FloatOperation'>:False, <class 'decimal.DivisionByZero'>:False, <class 'decimal.Overflow'>:False, <class 'decimal.Underflow'>:False, <class 'decimal.Subnormal'>:False, <class 'decimal.Inexact'>:False, <class 'decimal.Rounded'>:False, <class 'decimal.Clamped'>:False}
>>> +d
Decimal('5.00')
>>> c.flags
{<class 'decimal.InvalidOperation'>:False, <class 'decimal.FloatOperation'>:False, <class 'decimal.DivisionByZero'>:False, <class 'decimal.Overflow'>:False, <class 'decimal.Underflow'>:False, <class 'decimal.Subnormal'>:False, <class 'decimal.Inexact'>:True, <class 'decimal.Rounded'>:True, <class 'decimal.Clamped'>:False}

flagsの要素には辞書のようにアクセスできます。

>>> c.flags[Rounded]
True

フラグをクリアする

一旦設定されたフラグは明示的にクリアするまで残り続けるので、次の計算を始める前にフラグは全てクリアする必要があります。フラグをクリアするにはContextクラスのclear_flags()メソッドを使います。

>>> c.clear_flags()

例外を送出する

例外的な状態が発生したときにフラグがセットされますが、それと同時に例外を送出することもできます。それには、Contextオブジェクトのtraps属性に設定します。デフォルトでは、いくつかのシグナルで例外が送出するように設定されています。

次の例は、丸めが発生したときに例外を送出するように設定しています。

>>> getcontext().traps[Rounded] = True
>>> getcontext().traps
{<class 'decimal.InvalidOperation'>:True, <class 'decimal.FloatOperation'>:False, <class 'decimal.DivisionByZero'>:True, <class 'decimal.Overflow'>:True, <class 'decimal.Underflow'>:False, <class 'decimal.Subnormal'>:False, <class 'decimal.Inexact'>:False, <class 'decimal.Rounded'>:True, <class 'decimal.Clamped'>:False}

演算で丸めが発生すると、次のようにRounded 例外が送出されます。フラグも設定されますのでクリアを忘れないようにしましょう。

>>> d = Decimal('5.000')
>>> +d
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
decimal.Rounded: [<class 'decimal.Rounded'>]

シグナルの種類

decimalモジュールに定義されているシグナルには次のものがあります。

シグナル説明
decimal.InvalidOperation無効な演算が実行したときのシグナルです。たとえば「Decimal(‘Infinity’)-Decimal(‘Infinity’)」のような演算です。このシグナルを送出しない場合、演算結果として NaN を返します。
decimal.FloatOperationDecimalオブジェクト作成時の引数にfloat型を渡したり、Decimalオブジェクトとfloat型の比較など、Decimalオブジェクトとfloat型を混ぜて使用したことを通知します。シグナルを送出する場合でも、等号比較は可能です。
decimal.DivisionByZeroゼロで除算したときのシグナルです。除算やモジュロ除算などで起きます。このシグナルを送出しない場合、演算結果は Infinity または -Infinity になります。
decimal.Overflow数値オーバフローを示すシグナルです。シグナルをトラップしない場合、演算結果は値丸め方法に従って、表現可能な最大値になるかInfinity になります。いずれの場合も、 Inexact および Rounded が同時にシグナルされます。
decimal.Underflow演算によりアンダーフローが発生したことを示すシグナルです。演算結果が極めて小さく、値丸めによってゼロになった場合に発生します。Inexact および Subnormal シグナルも同時に発生します。
decimal.Subnormal値丸めを行う前に指数部が Emin より小さかったことを示すシグナルです。
演算結果が微小である場合 (指数が小さすぎる場合) に発生します。このシグナルをトラップしなければ、演算結果をそのまま返します。
decimal.Inexact演算結果が丸められたことによって、正確な演算結果が失われたことを通知します。このシグナルは値丸め操作中にゼロ以外の桁が失われた際に生じます。
decimal.Rounded値の丸めが発生したときのシグナルです。これは値丸めによって有効桁数が減少したときに常に発生します。
decimal.Clampedclampされた結果、指数部が変更されたことを通知します。

いくつかシグナルのフラグがセットされる例を見ていきましょう。これらの例を実行する前にフラグをクリアすることを忘れないでください。

丸め(Rounded)

次の例はRoundedフラグがセットされる例です。+演算子による演算を実行すると精度が3桁のため丸めが発生します。数としては演算前後で変わりませんが、丸めによって有効桁数の減少が発生しています。

>>> getcontext().prec = 3
>>> d = Decimal('3.000')
>>> +d
Decimal('3.00')
>>> getcontext()
Context(prec=3, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[Rounded], traps=[InvalidOperation, DivisionByZero, Overflow])

正確ではない演算結果(Inexact)

次の例はInexactフラグがセットされる例です。演算結果の正確な値は「3.001」ですが、丸めのため正確さが失われたのでInexactフラグがセットされます。Inexactフラグがセットされるときは常にRoundedフラグも同時にセットされます。

>>> d = Decimal('3.001')
>>> +d
Decimal('3.00')
>>> getcontext()
Context(prec=3, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[Inexact, Rounded], traps=[InvalidOperation, DivisionByZero, Overflow])

Decimalオブジェクトとfloat型の混合(FloatOperation)

Decimalオブジェクトとfloat型を混ぜて使うとFloatOperationフラグがセットされます。Decimalオブジェクトの作成時の引数にfloat型を使ったり、Decimalオブジェクトとfloat型の比較を行うとフラグがセットされます。

>>> Decimal(3.14)
Decimal('3.140000000000000124344978758017532527446746826171875')
>>> getcontext()
Context(prec=3, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[FloatOperation], traps=[InvalidOperation, DivisionByZero, Overflow])

FloatOperation例外を送出する場合でも、等号比較では例外を送出せず、フラグだけがセットされます。それ以外の比較演算では例外が送出されます。Decimalオブジェクトの作成時の引数にfloat型を渡しても例外が送出されます。

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

無効な演算

無効な演算ではInvalidOperationフラグがセットされます。

>>> Decimal(3) % 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
decimal.InvalidOperation: [<class 'decimal.InvalidOperation'>]
>>> getcontext()
Context(prec=3, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[InvalidOperation], traps=[InvalidOperation, DivisionByZero, FloatOperation, Overflow])

組み込み関数とDecimalオブジェクト

組み込みの数値型と同じようにDecimalオブジェクトを組み込み関数で使うこともできます。

組み込み型への変換

Decimalオブジェクトを組み込み型に変換するには、次の組み込み関数が使えます。

>>> str(Decimal('3.14'))
'3.14'
>>> float(Decimal('3.14'))
3.14
>>> int(Decimal('3.14'))
3

その他の組み込み関数

その他、Decimalオブジェクトを引数として渡すことができる組み込み関数の例です。

>>> decimals = list(map(Decimal, ['4.56', '2.34', '1.23', '3.45','5.67']))
>>> decimals
[Decimal('4.56'), Decimal('2.34'), Decimal('1.23'), Decimal('3.45'), Decimal('5.67')]
>>> min(decimals)
Decimal('1.23')
>>> max(decimals)
Decimal('5.67')
>>> sorted(decimals)
[Decimal('1.23'), Decimal('2.34'), Decimal('3.45'), Decimal('4.56'), Decimal('5.67')]

おわりに

ここでは紹介しきれませんでしたが、Decimalモジュールには多くのメソッドが定義されています。それらについてはドキュメントを参照してください。

よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

・PM、SE、SIなどを20年以上経験
・ネットワークスペシャリスト、セキュリティスペシャリストなど複数の資格を保有

目次