閉じる

Pythonで整数と小数を正確に計算する方法|float誤差・round・Decimalを総整理

Pythonで整数や小数を扱うとき、なんとなくintfloatを使っていると、思わぬ誤差やバグに悩まされます。

この記事では、「なぜ誤差が出るのか」「どう避ければよいか」を、図解とサンプルコードで丁寧に整理します。

金額計算などで失敗しないために、float誤差・round・Decimalの正しい使い方を一気に身につけましょう。

Pythonで数値型を正しく選ぶポイント

int・float・Decimalの違いと使い分け

まずはPythonでよく使う3つの数値型の性質を整理します。

Pythonで数値を扱うときは、最初に「どの型を使うか」を決めることがとても重要です。

それぞれの特徴を表にまとめます。

主な用途特徴
int1, 0, -5, 10_000個数、回数、ID、インデックス整数のみ・誤差なし、任意精度(桁数制限ほぼなし)
float0.1, 3.14, -2.5測定値、統計、機械学習、物理量高速だが誤差あり、2進数の近似表現
DecimalDecimal(“0.1”)など金額、税計算、為替、精密演算10進数ベースで高精度、速度はfloatより遅い

ざっくりした使い分けの指針

文章で整理すると、次のように考えると迷いにくくなります。

  • カウント・ID・インデックスのように、もともと小数が入り得ないものはintを使うべきです。ここでfloatを使う理由はほとんどありません。
  • 物理量や計測値、画像処理、機械学習など、多少の誤差があっても統計的に意味がある用途ではfloatを使うのが普通です。速度とメモリ効率を優先します。
  • 金額や税率、誤差が1円でも困る計算ではDecimalの利用を検討すべきです。特に請求書・給与計算・決済システムなどです。

このように、「誤差がどこまで許されるか」で型を選ぶことが、バグを防ぐ第一歩です。

なぜPythonのfloatに誤差が出るのか

Pythonのfloatは、概ねC言語のdoubleに相当し、2進数で小数を表現する「浮動小数点数」です。

ここに誤差の原因があります。

10進数では0.1はピッタリ表現できますが、2進数では0.1は無限に続く小数になります。

そのため、コンピュータ内部ではどこかで打ち切らざるを得ず、ごくわずかな誤差を含んだ「近似値」になります。

この誤差はとても小さいため、人間が通常の出力で見ると0.1に見えますが、計算を繰り返すと蓄積して目に見えるズレとなることがあります。

金額や精度が重要な場面での注意点

金額計算のように、「少数第2位まで」「1円単位で厳密に合うこと」が要求される場面でfloatをそのまま使うと、次のようなリスクがあります。

  • 1件ごとの計算では小数点以下の誤差が見えないが、大量件数の合計で1円以上のズレになる。
  • 合計金額と明細合計が一致せず、監査や顧客からのクレームにつながる。
  • システム間で金額を照合したときに、端数処理の違いで差異が出る。

このため、金額や点数など「有限桁でピッタリ表現したい数字」はDecimalで扱うことが重要です。

floatはあくまで、誤差を許容できる数値処理用と考えると安全です。

Pythonのfloat誤差を正しく理解する

2進数表現によるfloat誤差の仕組み

float誤差は、実はPython特有の問題ではありません。

ほとんどすべての一般的なプログラミング言語で起きる「浮動小数点数の宿命」です。

例として、0.1を2進数で表すとどうなるかを見てみます。

実際のビット列は難しいので、イメージだけ説明します。

  • 10進数の1/10(=0.1)は、2進数では0.0001100110011...「0011」が繰り返される無限小数になります。
  • しかし、コンピュータのメモリには有限のビット(例えば64ビット)しかないため、どこかで切り捨てる必要があります。
  • その結果、「0.1に限りなく近いがピッタリではない値」が内部で使われます。

この「ピッタリではない」という点が、後続の計算でジワジワと効いてきます。

よくあるfloat誤差の例と落とし穴

代表的な例をPythonで実際に確認してみます。

Python
# floatの誤差を確認するサンプル

a = 0.1
b = 0.2
c = a + b

print("a =", a)
print("b =", b)
print("a + b =", c)

# 期待は 0.3 だが...
print("a + b == 0.3 ?", c == 0.3)
実行結果
a = 0.1
b = 0.2
a + b = 0.30000000000000004
a + b == 0.3 ? False

典型的な落とし穴は次のようなものです。

  • 比較演算で==を使ってしまい、Trueにならない。
  • 合計値にわずかな誤差が混ざり、0.3のはずが0.30000000000000004と表示される。
  • 一度誤差が出てしまうと、繰り返し計算で誤差が蓄積しやすい。

「floatは誤差を含む近似値」という前提で設計し、誤差を前提としたコードを書くことが重要になります。

比較演算(==)と誤差許容比較(isclose)の使い方

float同士を比較するときに==を使うのは危険です。

Python 3.5以降では、math.iscloseを使って「十分近ければ同じとみなす」比較ができます。

Python
import math

x = 0.1 + 0.2
y = 0.3

print("x =", x)
print("y =", y)

# 危険な比較
print("x == y :", x == y)

# 安全な比較 (許容誤差つき)
print("math.isclose(x, y) :", math.isclose(x, y))

# 許容誤差(相対誤差, 絶対誤差)を自分で指定する例
print("厳しめの比較 :", math.isclose(x, y, rel_tol=1e-12, abs_tol=0.0))
実行結果
x = 0.30000000000000004
y = 0.3
x == y : False
math.isclose(x, y) : True
厳しめの比較 : True

数値計算でfloatを使うなら、「float比較はisclose」くらいの気持ちでいると安全です。

特にテストコードではassert x == yではなく、math.iscloseで比較する習慣をつけるとよいです。

round関数と小数処理の基本

Pythonのroundの仕様と注意点

Pythonのroundは、「0.5は常に切り上げる」わけではない点に注意が必要です。

Pythonのroundは「偶数への丸め(銀行丸め, banker’s rounding)」を採用しています。

Python
print(round(2.5))  # 期待通り? 3ではない
print(round(3.5))
print(round(1.5))
print(round(2.5))
print(round(4.5))
実行結果
2
4
2
2
4

これは、0.5ちょうどの場合、一番近い偶数へ丸めるというルールです。

統計的にバイアスを減らすために採用されていますが、「常に0.5を切り上げ」と思い込んでいるとバグの原因になります。

桁数を指定した四捨五入・切り上げ・切り捨て

roundには第2引数で桁数を指定できます。

Python
value = 123.4567

print(round(value, 0))  # 小数第1位で四捨五入 → 整数に
print(round(value, 1))  # 小数第2位で四捨五入
print(round(value, 2))  # 小数第3位で四捨五入
print(round(value, -1)) # 10の位で四捨五入
print(round(value, -2)) # 100の位で四捨五入
実行結果
123.0
123.5
123.46
120.0
100.0

一方で、「切り上げ」「切り捨て」をしたい場合mathモジュールを使うのが定石です。

Python
import math

x = 12.345

# 切り捨て
print("floor(x)      =", math.floor(x))      # 12
# 切り上げ
print("ceil(x)       =", math.ceil(x))       # 13
# 0方向へ切り捨て (正ならfloor, 負ならceilと同じ方向)
print("truncate(x)   =", math.trunc(x))      # 12

# 小数第2位で切り捨てをしたい場合は、一旦10倍してから戻す
print("小数第2位で切り捨て =", math.floor(x * 100) / 100)
実行結果
floor(x)      = 12
ceil(x)       = 13
truncate(x)   = 12
小数第2位で切り捨て = 12.34

ただし、ここでもfloat誤差の影響を受けるため、金額のような厳密さが必要な場面ではDecimalを使った方が安全です。

金融計算でroundだけに頼ってはいけない理由

金融計算では、「どのタイミングで丸めるか」が結果に大きく影響します。

たとえば、次のようなケースを考えます。

Python
# 商品ごとに税込価格を計算する例
prices = [100.0, 200.0, 300.0]  # 税抜価格
tax_rate = 0.1                  # 消費税10%

# パターンA: 各行をroundしてから合計
total_a = 0
for p in prices:
    price_with_tax = p * (1 + tax_rate)
    total_a += round(price_with_tax)  # 各行を四捨五入(整数円に)

# パターンB: 合計を出してから最後にround
subtotal = sum(prices)
total_b = round(subtotal * (1 + tax_rate))

print("パターンA(行ごとに丸め) :", total_a)
print("パターンB(まとめて丸め) :", total_b)
実行結果
パターンA(行ごとに丸め) : 660
パターンB(まとめて丸め) : 660

この例ではたまたま同じになりますが、端数が出る価格の組み合わせでは1円以上の差が生まれることがあります。

重要なのは次の点です。

  • ビジネスルールとして「行ごと丸め」なのか「合計で丸め」なのかを決めておく。
  • 「roundしておけば安全」ではない。丸めの仕様をドキュメント化し、システム全体で統一する。
  • 浮動小数点の誤差に加えて、丸めタイミングの違いでも結果がブレるため、Decimalと一貫した丸めルールの組み合わせが必要になる。

Decimalで整数・小数を正確に計算する

Decimalの基本

decimal.Decimalは、10進数の世界で高精度な計算をするためのクラスです。

金額や税率など、誤差が許されない場面で活躍します。

基本的な使い方は次のようになります。

Python
from decimal import Decimal

# Decimalの基本
a = Decimal("0.1")  # 文字列から生成するのが重要(理由は後述)
b = Decimal("0.2")
c = a + b

print("a =", a)
print("b =", b)
print("a + b =", c)
print("a + b == Decimal('0.3') ?", c == Decimal("0.3"))
実行結果
a = 0.1
b = 0.2
a + b = 0.3
a + b == Decimal('0.3') ? True

0.1 + 0.2 がピッタリ 0.3 になることが確認できます。

これはDecimalが10進数の桁をそのまま扱うためです。

floatからではなく文字列からDecimalを作る理由

Decimalを使うときにもっとも大事なポイントが、floatから直接Decimalを作らないことです。

Python
from decimal import Decimal

# 悪い例: floatからDecimalを作る
x_float = 0.1
x_dec_from_float = Decimal(x_float)

# 良い例: 文字列からDecimalを作る
x_dec_from_str = Decimal("0.1")

print("floatの0.1           :", x_float)
print("Decimal(0.1)        :", x_dec_from_float)
print("Decimal('0.1')      :", x_dec_from_str)
print("どちらが等しいか?  :", x_dec_from_float == x_dec_from_str)
実行結果
floatの0.1           : 0.1
Decimal(0.1)        : 0.1000000000000000055511151231
Decimal('0.1')      : 0.1
どちらが等しいか?  : False

この例からわかるように、floatの誤差をDecimalに「持ち込んで」しまうと、せっかくの高精度が台無しになります。

そのため、外部から受け取る入力(文字列)の段階でDecimalに変換することが重要です。

WebフォームやCSVなどの文字列データで受け取り、すぐにDecimal("...")にしてしまうのが安全なパターンです。

Decimalで金額計算を正確に行う実践パターン

典型的な税込金額計算を、Decimalで書いてみます。

Python
from decimal import Decimal, ROUND_HALF_UP, getcontext

# コンテキスト(精度や丸めモード)の設定
# 必要に応じて精度を高める(ここでは28桁のまま)
getcontext().prec = 28

# 入力は文字列から受け取る想定
price_str = "1980"   # 税抜価格(円)
tax_rate_str = "0.10"  # 消費税率 10%

# 文字列からDecimalを作る
price = Decimal(price_str)
tax_rate = Decimal(tax_rate_str)

# 税額と税込価格を計算(ここでは小数第2位まで保持)
tax = (price * tax_rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
price_with_tax = (price + tax).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

print("税抜価格   :", price)           # 1980
print("税額(円)   :", tax)             # 198.00
print("税込価格   :", price_with_tax)  # 2178.00
実行結果
税抜価格   : 1980
税額(円)   : 198.00
税込価格   : 2178.00

ポイントは次の通りです。

  • 最初から最後までDecimalだけで完結させる。
  • 税率もDecimalで表現し、途中でfloatに変換しない。
  • .quantize(Decimal("0.01"), rounding=...)小数第2位までに丸める。これが実務でよく使うパターンです。

Decimalとroundの組み合わせ方と丸めモード

Decimalには独自の丸めメソッドquantizeがあり、丸めモードを細かく指定できます。

roundと混同しやすいですが、Decimalを使うならquantizeを使うのが基本です。

Python
from decimal import Decimal, ROUND_HALF_UP, ROUND_DOWN, ROUND_UP

x = Decimal("123.4567")

# 小数第2位までに四捨五入(いわゆる一般的な四捨五入)
y_half_up = x.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

# 小数第2位までに切り捨て
y_down = x.quantize(Decimal("0.01"), rounding=ROUND_DOWN)

# 小数第2位までに切り上げ
y_up = x.quantize(Decimal("0.01"), rounding=ROUND_UP)

print("元の値           :", x)
print("四捨五入(HALF_UP):", y_half_up)
print("切り捨て(DOWN)  :", y_down)
print("切り上げ(UP)    :", y_up)
実行結果
元の値           : 123.4567
四捨五入(HALF_UP): 123.46
切り捨て(DOWN)  : 123.45
切り上げ(UP)    : 123.46

主な丸めモードには次のようなものがあります。

定数名意味よくある用途
ROUND_HALF_UP一般的な四捨五入(0.5以上切り上げ)日本での金額四捨五入など
ROUND_HALF_DOWN0.5は切り捨て、それより大きければ切り上げ特殊な業務ルール
ROUND_HALF_EVEN偶数への丸め(Pythonのroundと同じ)統計・会計の一部
ROUND_UP常に0から離れる方向に丸め(絶対値増加)安全側に倒す計算
ROUND_DOWN常に0に近づく方向に丸め(絶対値減少)顧客有利な切り捨てなど

業務要件に応じてどの丸めモードを使うかを明示し、コード上でも定数で指定することで、「なんとなくroundした結果」が変わってしまうリスクを減らせます。

パフォーマンスと精度のトレードオフ

Decimalは高精度ですが、floatに比べると計算速度が遅いというデメリットがあります。

大まかな感覚として、数倍〜十数倍程度遅くなるケースもあります。

Python
import time
from decimal import Decimal

# 試しに簡易ベンチマーク(おおよその感覚)
N = 1_00000

start = time.time()
x = 0.0
for _ in range(N):
    x += 0.1
end = time.time()
print("float   結果:", x, "時間:", end - start)

start = time.time()
y = Decimal("0.0")
d = Decimal("0.1")
for _ in range(N):
    y += d
end = time.time()
print("Decimal結果:", y, "時間:", end - start)

※実際の時間は環境によって大きく異なります。

金融システムを設計するときには、次のような考え方が現実的です。

  • ビジネス的に意味のある金額はDecimalで管理する。(請求金額、残高、税額など)
  • 機械学習用の特徴量計算や統計処理など、多少の誤差があっても問題ない部分はfloatで計算する。
  • 途中でfloatとDecimalを混ぜない。どうしても混在させる場合は、境界を明確にし、変換ルールを決める。

「全部Decimal」にすれば安全ですが、性能や開発コストとのバランスも考えて、どこまで厳密さが必要かを判断することが重要です。

まとめ

Pythonで整数と小数を正確に扱うには、「どの型を使うか」「誤差と丸めをどう管理するか」を意識することが欠かせません。

整数カウントにはint、誤差許容の数値計算にはfloat、金額や高精度計算にはDecimalと役割分担し、比較にはmath.isclose、Decimalではquantizeと丸めモードを使い分けます。

この記事の内容を押さえておけば、float誤差に悩まされることなく、安心してPythonで数値計算を設計できるようになります。

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

URLをコピーしました!