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に変換して確認してみます。
# 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はどちらも内部で近似値として保持されるため、足し算の結果も近似値になります。
# 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
が用意されています。
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
累積誤差にも注意する
同じ誤差が繰り返し加わると、ズレは蓄積します。
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進の有限桁として正確に保持できます。
四捨五入のルールや精度も柔軟に設定できます。
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を作ると誤差ごと持ち込みます。
from decimal import Decimal
bad = Decimal(0.1) # または Decimal.from_float(0.1)
print(bad)
print(bad == Decimal('0.1')) # 等価にはならない
0.1000000000000000055511151231257827021181583404541015625
False
良い例: 文字列リテラルから作る
外部入力も内部定数も、常に文字列でDecimalにするのが基本です。
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が欲しければ文字列から作るべきです。
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
で桁を固定して誤差を落とします。
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で与えるのが安全です。
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
などから選びます。
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が便利です。
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桁や整数に丸める例です。
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
明細ごとに丸めるか、合算後に丸めるか
会計では「明細ごと丸め」か「合計後丸め」かの方針を揃える必要があります。
結果が変わることがあるからです。
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になりますし、仮に暗黙変換しても誤差を招きます。
必ずどちらかに統一してください。
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より遅いです。
目安を把握し、必要な箇所にだけ適用しましょう。
# 実行時間は環境で大きく変わります。比率の目安として参照してください。
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に変換します。
フォーマットも同時に検証しやすく、安全です。
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として読み込めます。
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は型が緩いので、文字列で保持するか、明示的にアダプタを登録します。
# 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では表現を統一する、パフォーマンスに留意するといった実務の注意点を守れば、安心して正確な小数計算を行えます。