閉じる

Pythonのfloatで誤差が出る理由とDecimalでの解決法

Pythonの数値計算は手軽で高速ですが、実は小数の扱いに落とし穴があります。

とくに0.1や0.2のような身近な数字でさえ、計算結果が直観と異なることがあります。

本記事ではなぜPythonのfloatで誤差が出るのかを丁寧に説明し、Decimalモジュールで誤差を避ける実践的な方法を、初心者の方にも分かりやすくサンプルコード付きで解説します。

Pythonのfloatで誤差が出る理由

2進数表現では0.1を正確に表せない

10進小数は2進小数で循環することがある

コンピュータは内部で数値を2進数で表現します。

ところが10進数で有限桁の小数であっても、2進数では循環小数になってしまうものが多く、0.1はその代表例です。

つまり0.1は2進数では無限に続く数で、途中で丸めて格納するしかありません。

この丸めが誤差の出発点です。

実際の内部表現を覗いてみる

見た目は0.1でも、内部では僅かにズレています。

17桁表示やDecimalに変換して確認してみます。

Python
# 0.1の内部的なズレを確認する例
from decimal import Decimal

# 17桁で表示すると、丸め誤差が見えることが多い
print(format(0.1, '.17f'))

# float -> Decimal に正確な値を移すとズレが露わになる
print(Decimal.from_float(0.1))  # 非推奨な取り込み方の例示
実行結果
0.10000000000000001
0.1000000000000000055511151231257827021181583404541015625

このズレ自体はバグではなく、浮動小数点数の仕様です

Pythonに限らず、IEEE 754に基づく多くの言語で同様に起きます。

0.1+0.2が0.3にならない理由

実例で確認する

0.1と0.2はどちらも内部で近似値として保持されるため、足し算の結果も近似値になります。

Python
# 0.1 + 0.2 の挙動
a = 0.1 + 0.2
print(a)
print(format(a, '.17f'))
print(a == 0.3)
実行結果
0.30000000000000004
0.30000000000000004
False

表示丸めと内部値の違い

printはPython 3系で「人にとって分かりやすい最短の10進表記」を出すため、場合によっては誤差が目立ちにくい表示になります。

しかし等価比較や高精度表示を行うと誤差が露見します。

等価比較(==)の落とし穴

許容誤差を含めて比較する

浮動小数点では「だいたい等しい」かどうかを比較するのが定石です。

Pythonではmath.iscloseが用意されています。

Python
import math

print(math.isclose(0.1 + 0.2, 0.3))  # デフォルト許容誤差
print(math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-12, abs_tol=0.0))  # 許容誤差を厳しめに
実行結果
True
True

累積誤差にも注意する

同じ誤差が繰り返し加わると、ズレは蓄積します。

Python
s = sum([0.1] * 10)
print(s)
print(format(s, '.17f'))
print(s == 1.0, math.isclose(s, 1.0))
実行結果
0.9999999999999999
0.99999999999999989
False True

金額や請求書の集計などでこの誤差は致命的になりえます

そこでDecimalの出番です。

Decimalで計算誤差を避ける

Decimalとは(10進数で正確に計算)

金融や会計のための10進小数

decimal.Decimalは10進表現をそのまま扱える高精度小数です。

2進では循環してしまう0.1も、10進の有限桁として正確に保持できます。

四捨五入のルールや精度も柔軟に設定できます。

Python
from decimal import Decimal

x = Decimal('0.1')
y = Decimal('0.2')
print(x + y)              # 誤差なし
print((x + y) == Decimal('0.3'))
実行結果
0.3
True

文字列で初期化するのが安全

悪い例: float経由で初期化

floatを経由してDecimalを作ると誤差ごと持ち込みます

Python
from decimal import Decimal

bad = Decimal(0.1)               # または Decimal.from_float(0.1)
print(bad)
print(bad == Decimal('0.1'))     # 等価にはならない
実行結果
0.1000000000000000055511151231257827021181583404541015625
False

良い例: 文字列リテラルから作る

外部入力も内部定数も、常に文字列でDecimalにするのが基本です。

Python
from decimal import Decimal

good = Decimal('0.1')
print(good)
print(good == Decimal('0.1'))
実行結果
0.1
True

floatからの変換(from_float)の注意

近似値のまま取り込むことを理解する

Decimal.from_floatは「そのfloatが表す近似値」を十進に展開するだけです。

正確な0.1が欲しければ文字列から作るべきです。

Python
from decimal import Decimal

d1 = Decimal.from_float(0.1)
d2 = Decimal('0.1')
print(d1)
print(d1 == d2)
実行結果
0.1000000000000000055511151231257827021181583404541015625
False

どうしてもfloatを受けるなら正規化する

floatを受け取るAPIの都合があるなら、用途に応じてquantizeで桁を固定して誤差を落とします。

Python
from decimal import Decimal, ROUND_HALF_UP

as_float = Decimal.from_float(0.1)
normalized = as_float.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)  # 小数2桁に丸め
print(normalized)  # => 0.10
実行結果
0.10

基本の演算(+,-,*,/)

演算は自然に使える

加減乗除や比較は普通に使えます。

合計には初期値をDecimalで与えるのが安全です。

Python
from decimal import Decimal, getcontext

getcontext().prec = 28  # デフォルト精度。必要に応じて調整
a = Decimal('1.50')
b = Decimal('2.25')

print(a + b)               # 加算
print(b - a)               # 減算
print(a * b)               # 乗算
print(b / a)               # 除算(精度依存)
print(sum([Decimal('0.1')] * 10, Decimal('0')))
実行結果
3.75
0.75
3.3750
1.5
1.0

精度と丸めの設定(Decimal)

getcontextで精度(prec)と丸め(rounding)を指定

精度は「有効桁数」

getcontext().precは有効桁数を表します。

丸め規則はROUND_HALF_UPなどから選びます。

Python
from decimal import Decimal, getcontext, ROUND_HALF_UP, ROUND_HALF_EVEN

# 精度と丸めの設定
ctx = getcontext()
ctx.prec = 10
ctx.rounding = ROUND_HALF_EVEN  # 銀行丸め

print(Decimal('1') / Decimal('7'))   # 精度10桁での1/7
実行結果
0.1428571429

よく使う丸め規則の概要

  • ROUND_HALF_UP: 5以上を切り上げる(一般的な四捨五入)
  • ROUND_HALF_EVEN: 5は偶数側に丸める(銀行丸め)
  • ROUND_DOWN: 0方向へ切り捨て

用途に応じて選択します。

quantizeで小数点以下の桁数を固定

パターンで桁数を指定する

金額や測定値の表示桁を固定するならquantizeが便利です。

Python
from decimal import Decimal, ROUND_HALF_UP, ROUND_DOWN

x = Decimal('1.2349')
print(x.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))  # 小数2桁に四捨五入
print(x.quantize(Decimal('0.01'), rounding=ROUND_DOWN))     # 小数2桁で切り捨て
print(Decimal('123').quantize(Decimal('1')))                # 整数に固定
実行結果
1.23
1.23
123

金額計算(消費税/端数処理)の例

1行の金額と税込の丸め

税込価格を小数2桁や整数に丸める例です。

Python
from decimal import Decimal, ROUND_HALF_UP, ROUND_HALF_EVEN, ROUND_DOWN

price = Decimal('1980')
tax_rate = Decimal('0.10')  # 10%

gross_exact = price * (Decimal('1') + tax_rate)  # 正確値は 2178.0
print('正確値:', gross_exact)

# 表示や会計上の丸め
print('四捨五入(2桁):', gross_exact.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))
print('四捨五入(整数):', gross_exact.quantize(Decimal('1'), rounding=ROUND_HALF_UP))
print('銀行丸め(整数):', gross_exact.quantize(Decimal('1'), rounding=ROUND_HALF_EVEN))
print('切り捨て(整数):', gross_exact.quantize(Decimal('1'), rounding=ROUND_DOWN))
実行結果
正確値: 2178.0
四捨五入(2桁): 2178.00
四捨五入(整数): 2178
銀行丸め(整数): 2178
切り捨て(整数): 2178

明細ごとに丸めるか、合算後に丸めるか

会計では「明細ごと丸め」か「合計後丸め」かの方針を揃える必要があります。

結果が変わることがあるからです。

Python
from decimal import Decimal, ROUND_HALF_UP

items = [Decimal('1980'), Decimal('980'), Decimal('120')]
tax_rate = Decimal('0.10')

# 1. 明細ごとに税込を整数丸めして合計
line_rounded = [ (p * (1 + tax_rate)).quantize(Decimal('1'), rounding=ROUND_HALF_UP) for p in items ]
total_line = sum(line_rounded, Decimal('0'))

# 2. 税抜合計してから税込を整数丸め
subtotal = sum(items, Decimal('0'))
total_after = (subtotal * (1 + tax_rate)).quantize(Decimal('1'), rounding=ROUND_HALF_UP)

print('明細ごと丸め合計:', total_line, '内訳:', line_rounded)
print('合計後に丸め:', total_after)
実行結果
明細ごと丸め合計: 3388 内訳: [Decimal('2178'), Decimal('1078'), Decimal('132')]
合計後に丸め: 3388

この例では一致しましたが、端数が絡むと差が出るケースもあります。

どちらを採用するか仕様で明確にしましょう。

実務での使い分けと注意点

金額や測定値はDecimalを選ぶ

金額や請求、在庫数量、小数桁が意味を持つ測定値では必ずDecimalを使います。

floatは速度に優れますが厳密さが必要な領域には不向きです。

一方で、機械学習やグラフィックスなどの数値計算ではfloatの方が現実的です。

以下に目安をまとめます。

シーン代表例推奨型理由
金融・会計価格、税額、割引Decimal桁の固定と丸め規則が必須で誤差が許容されない
科学・工学物理シミュレーションfloat高速でメモリ効率が良く、相対誤差で十分
データ表示レポートでの固定小数表示Decimal表示桁と丸めのポリシーを厳密に管理
機械学習行列演算、最適化floatハードウェア最適化が豊富で桁落ち対策も慣例あり

型を混在させない(floatとDecimalの演算は避ける)

Decimalとfloatを直接混在させるとTypeErrorになりますし、仮に暗黙変換しても誤差を招きます。

必ずどちらかに統一してください。

Python
from decimal import Decimal

try:
    print(Decimal('1.1') + 1.1)  # 型混在
except TypeError as e:
    print('TypeError:', e)

# 安全なパターン: どちらかに統一
print(Decimal('1.1') + Decimal('1.1'))
print(float(1.1) + float(1.1))  # 用途次第だが誤差は許容前提
実行結果
TypeError: unsupported operand type(s) for +: 'decimal.Decimal' and 'float'
2.2
2.2

パフォーマンスの注意

Decimalは汎用的で高機能な代わりにfloatより遅いです。

目安を把握し、必要な箇所にだけ適用しましょう。

Python
# 実行時間は環境で大きく変わります。比率の目安として参照してください。
import timeit
from decimal import Decimal, getcontext
getcontext().prec = 28

t_float = timeit.timeit('sum(i/10 for i in range(10000))', number=1000)
t_dec = timeit.timeit('sum(Decimal(i)/Decimal(10) for i in range(10000))', number=1000, globals=globals())

print('float秒:', t_float)
print('Decimal秒:', t_dec)
print('倍率(Decimal/float):', t_dec / t_float)
実行結果
float秒: 0.26877490000333637
Decimal秒: 2.243839599977946
倍率(Decimal/float): 8.348397115765247

20倍程度遅いことも珍しくありません。

ボトルネックにならない範囲で使い分けましょう。

入出力時の扱い(文字列/JSON/DB)

文字列との往復

外部からの入力は必ず文字列で受けてDecimalに変換します。

フォーマットも同時に検証しやすく、安全です。

Python
from decimal import Decimal, InvalidOperation

raw = '1234.50'
try:
    amount = Decimal(raw)
    print('OK:', amount)
except InvalidOperation:
    print('数値ではありません')
実行結果
OK: 1234.50

JSONと連携する場合

標準のjsonはDecimalをそのままは扱えません。

  • 出力する場合はdefault=strで文字列化するか、用途に応じてfloatに変換します。
  • 入力する場合はjson.loads(..., parse_float=Decimal)で数値をDecimalとして読み込めます。
Python
import json
from decimal import Decimal

data = {'price': Decimal('123.45')}

# JSONへ: 数値でなく文字列として出力する場合
s = json.dumps(data, default=str, ensure_ascii=False)
print(s)  # {"price": "123.45"}

# JSONから: 数値トークンをDecimalで読み込む
s2 = '{"price": 123.45, "qty": 2}'
loaded = json.loads(s2, parse_float=Decimal)
print(loaded, type(loaded['price']))
実行結果
{"price": "123.45"}
{'price': Decimal('123.45'), 'qty': 2} <class 'decimal.Decimal'>
注意

default=strで出力した場合はJSON内の価格が「文字列」になります。

後段のシステムが数値として扱う必要があるなら、取り決めを合わせてください。

データベースの型

RDBMSではDECIMALまたはNUMERIC型を使います。

Pythonの多くのDBドライバはこれらのカラムをDecimalとして受け渡しできます。

SQLiteは型が緩いので、文字列で保持するか、明示的にアダプタを登録します。

Python
# SQLiteでDecimalを文字列として安全に保存する例
import sqlite3
from decimal import Decimal

sqlite3.register_adapter(Decimal, lambda d: str(d))  # 書き込む時は文字列化

conn = sqlite3.connect(':memory:')
conn.execute('CREATE TABLE t (price TEXT)')
conn.execute('INSERT INTO t (price) VALUES (?)', (Decimal('123.45'),))
row = conn.execute('SELECT price FROM t').fetchone()
print(row[0])  # 文字列として戻るので、アプリ側でDecimalに戻す
print(Decimal(row[0]))
実行結果
123.45
123.45

大規模システムでは、API層・DB層・フロントエンドで小数の表現と丸め規則を統一することが重要です。

まとめ

floatは高速だが近似値、Decimalは厳密だが重いという性質を理解し、用途に応じて正しく選ぶことが大切です。

0.1+0.2が0.3にならないのは2進数による丸め誤差が原因で、等価比較はmath.iscloseを使うのが定石です。

金融や金額計算ではDecimalを文字列から生成し、getcontextやquantizeで精度と丸めを明示します。

さらに、型を混在させない、I/Oでは表現を統一する、パフォーマンスに留意するといった実務の注意点を守れば、安心して正確な小数計算を行えます。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!