PythonのfloatとDecimalで0.1はどう違う?

0.1と表示されても、正確な0.1とは限らない

Pythonで小数を計算するとき、画面に見える数字と、実際に保持されている値を区別する必要がある。decimalの公式ドキュメントでは、0.1 + 0.1 + 0.1 - 0.3は十進浮動小数点で正確にゼロになる一方、二進浮動小数点ではゼロに近い小さな差が残ると説明されている。

ここで確認したいのは、表示を何桁にするかだけではない。計算に使う値が、十進数として正確に保持されているかどうかだ。floatで表示を整えることと、Decimalで十進数を扱うことでは、変えている対象が違う。

公式チュートリアルによると、十進小数の多くは二進小数として正確に表せない。0.1もその一つで、有限の二進桁で表すと近似になる。多くの環境で、保存される値は3602879701896397 / 2**55という二進分数であり、正確な十分の一とは異なる。

出典:Python公式チュートリアル:浮動小数点演算、decimal公式ドキュメント(取得日:2026-10-19)

公式が説明する「表示」と「保存値」の違い

公式チュートリアルは、表示が正確な十分の一に見えても、実際に保存されているのは表現可能な最も近い二進分数だと説明している。

この違いは、0.1の表示を考えると分かりやすい。保存された二進近似値を十進数で正確に書き出すと、多くの環境では次の値になる。

0.1000000000000000055511151231257827021181583404541015625

通常の表示では、この長い数字列の代わりに0.1が使われる。短く表示できることと、保存値が十進数の十分の一に一致することは別である。文字列の書式指定で表示桁数を制限する場合も、行っているのは保存値の表示を丸める処理だ。

一方、decimalの公式ドキュメントは、十進数を正確に表現できることを特徴として挙げている。また、末尾のゼロを有効な桁として保持する。たとえば、1.30と1.20の加算結果は2.50になり、入力の桁の意味を結果にも残す。

乗算にも、この桁の扱いが表れる。公式の説明では、1.3 * 1.2は1.56、1.30 * 1.20は1.5600になる。数値として同じ大きさでも、末尾のゼロを含む表現には違いがある。

ただし、「Decimalなら正確」という説明だけでは、生成時の重要な条件が抜ける。文字列から生成する場合と、すでに存在するfloatから生成する場合では、受け取る値が違う。

出典:Python公式チュートリアル:浮動小数点演算、decimal公式ドキュメント(取得日:2026-10-19)

float・文字列入力・float入力を比べる

値の表現と入力の渡し方に分けると、違いを整理できる。

対象 保持・変換する値 精度についての規定
floatの0.1 表現可能な最も近い二進分数 十進数の十分の一を正確には表せない
Decimal('3.14') 文字列で指定した十進数 保存する桁数は入力の桁数で決まる
Decimal(3.14) 渡されたfloatの値を正確に十進変換したもの 生成時の保存桁数はコンテキスト精度で制限されない
Decimal同士の加算・除算など 十進浮動小数点による演算 コンテキストの精度と丸め規則が適用される

この表で注意したいのは、floatからの変換も「正確な変換」だという点だ。変換対象は、すでに二進浮動小数点として保持されている値である。

公式ドキュメントには、次の生成例が掲載されている。

>>> from decimal import Decimal
>>> Decimal('3.14')
Decimal('3.14')
>>> Decimal(3.14)
Decimal('3.140000000000000124344978758017532527446746826171875')

文字列の'3.14'を渡すと、指定した十進数が保存される。一方、数値の3.14を渡すと、そのfloatが保持していた値が正確に変換される。Decimalに変換しても、経由したfloatの値が文字列入力と同じ値に戻るわけではない。

十進数として指定した値を保持したい場合は、文字列から生成する方法を使える。入力を確認するときは、Decimalの呼び出しだけでなく、その引数が文字列なのか、すでにfloatになっているのかも確認したい。

出典:decimal公式ドキュメント:Decimal objects(取得日:2026-10-19)

表示用の文字列と、丸めたDecimalを分ける

decimalの公式ドキュメントには、Decimal('2.675')をformat()やf文字列で整形する例が掲載されている。固定小数点形式のfと、小数点以下2桁を指定する.2fでは、次のように表示が変わる。

>>> from decimal import Decimal
>>> format(Decimal('2.675'), 'f')
'2.675'
>>> format(Decimal('2.675'), '.2f')
'2.68'
>>> f"{Decimal('2.675'):.2f}"
'2.68'

これらの結果は、表示に使う文字列である。Decimalオブジェクトは変更不能なので、書式指定によって元のオブジェクトの値が書き換わるわけではない。

この整形では、丸め規則も結果に関わる。公式には、localcontext()で丸め規則をROUND_DOWNに設定すると、同じDecimal('2.675')の小数点以下2桁の表示が2.67になる例もある。

>>> from decimal import localcontext, ROUND_DOWN
>>> with localcontext() as ctx:
...     ctx.rounding = ROUND_DOWN
...     print(format(Decimal('2.675'), '.2f'))
...
2.67

十進演算を選ぶ理由については、decimalの公式ドキュメントが説明している。十進浮動小数点では0.1 + 0.1 + 0.1 - 0.3が正確にゼロになり、厳密な等値関係が必要な会計用途で好まれるとしている。ただし、演算結果の精度や丸めについては別途確認が必要だ。

結果を固定の小数桁に丸め、Decimalとして得たい場合は、quantize()を使える。公式では、指定した指数へ丸めるメソッドとして説明されており、次の例が掲載されている。

>>> from decimal import Decimal, ROUND_DOWN, ROUND_UP
>>> Decimal('7.325').quantize(Decimal('.01'), rounding=ROUND_DOWN)
Decimal('7.32')
>>> Decimal('7.325').quantize(Decimal('1.'), rounding=ROUND_UP)
Decimal('8')

この例では、丸め先を表すDecimalと丸め規則を引数で指定している。最初の例は小数点以下2桁への丸め、次の例は整数の桁への丸めである。表示用の文字列を作る処理と、丸めたDecimalを得る処理を分けて読むと、それぞれの役割を確認しやすい。

出典:decimal公式ドキュメント:Quick-start tutorial、Decimal objects(取得日:2026-10-19)

Decimalにも生成時と演算時の違いがある

Decimalのコンテキスト精度は、生成する値の保存桁数を制限する設定ではない。公式では、新しく生成されるDecimalの有効な桁は入力の桁数だけで決まり、コンテキストの精度と丸めは演算時に関わると説明されている。

公式の例では、精度を6に設定しても、文字列から生成した値には入力した桁が保持される。一方、その値を使った加算結果には精度設定が適用される。

>>> from decimal import Decimal, getcontext
>>> getcontext().prec = 6
>>> Decimal('3.1415926535')
Decimal('3.1415926535')
>>> Decimal('3.1415926535') + Decimal('2.7182818285')
Decimal('5.85987')

このprec = 6は、小数点以下を6桁にそろえる指定ではない。上の加算結果では、整数部分も含めて6桁になっている。固定の小数桁への丸めを確認したい場合は、前節のquantize()の例と区別する必要がある。

同じ精度でも、丸め規則によって演算結果は変わる。公式の例では、丸め規則をROUND_UPに変更して同じ加算を行うと、結果はDecimal('5.85988')になる。コンテキストを確認するときは、精度の数値だけでなく丸め規則も見る必要がある。

Decimalでも、あらゆる演算結果を有限の桁で正確に表せるわけではない。公式には、Decimal(1) / Decimal(7)の結果が、精度6では0.142857、精度28では0.1428571428571428571428571429になる例がある。入力値を正確に保持することと、演算結果をどの精度で求めるかは、それぞれ確認したい点だ。

出典:decimal公式ドキュメント(取得日:2026-10-19)

floatとの混在は、演算と比較で扱いが違う

Decimalとfloatは、一般に混ぜて算術演算できない。公式では、両者を加算しようとするとTypeErrorになると説明されている。一方で、数値型をまたぐ比較は可能だ。比較できることを根拠に、同じ組み合わせで加算もできるとは判断できない。

意図しないfloatの混入を検出する設定としては、FloatOperationのトラップがある。これを有効にすると、floatを渡したコンストラクターや、Decimalとfloatの大小比較で例外が発生する。

ただし、このトラップは既定で無効であり、有効にしても等値比較まで一律に禁止されるわけではない。公式の例では、トラップが有効な状態でもDecimal('3.5') == 3.5はTrueになる。どの操作を検出する設定なのかを確認して使う必要がある。

また、コンテキストには、演算中に発生した状態を記録するフラグもある。公式の例では、精度9でDecimal(355) / Decimal(113)を計算すると、結果はDecimal('3.14159292')となり、InexactとRoundedが記録される。捨てられた桁にゼロ以外があり、丸めによって結果が不正確になったことを確認できる。

フラグは明示的に消去するまで残る。公式では、監視する計算のまとまりごとに、先にclear_flags()で消去する方法が示されている。値の生成方法や精度設定に加え、演算時に丸めが起きたかを調べる手段も用意されている。

出典:decimal公式ドキュメント(取得日:2026-10-19)

自分の環境で確かめる手順

次のコマンドでは、十進演算と二進演算の差、文字列入力とfloat入力の違い、精度設定が生成時と演算時に与える影響を順に確認できる。

python -c "from decimal import Decimal; print(0.1 + 0.1 + 0.1 - 0.3); print(Decimal('0.1') + Decimal('0.1') + Decimal('0.1') - Decimal('0.3'))"
python -c "from decimal import Decimal; print(Decimal('3.14')); print(Decimal(3.14))"
python -c "from decimal import Decimal, getcontext; getcontext().prec = 6; print(Decimal('3.1415926535')); print(Decimal('3.1415926535') + Decimal('2.7182818285'))"

最初のコマンドでは、差を求める計算結果に注目する。二つ目では、同じ3.14という数字を記述していても、文字列とfloatで生成される値が異なることを確認する。最後のコマンドでは、精度を設定した後の生成値と加算結果を見比べる。

表示の長さだけで判断せず、「何から生成した値か」「演算を行った後か」「表示だけを整えたものか」を分けて読むことが、公式の説明を自分のコードに当てはめる確認になる。

コメント

タイトルとURLをコピーしました