Pythonの丸め誤差とは?初心者にもわかる原因と対策をエンジニア歴10年の視点で解説
Pythonで計算していると、なぜか結果が少しズレることがあります。
たとえば、0.1 + 0.2 を計算しただけなのに、結果が 0.30000000000000004 になる。初めて見ると、Pythonが間違っているのでは?と思いますよね。
でも、これはPythonのバグではありません。コンピューターが小数を扱う仕組みによって起きる、丸め誤差という現象です。
この記事では、Python 丸め誤差について、IT初心者にもわかりやすく解説します。
サンプルコードを交えながら、なぜ起きるのか、どんな場面で注意すべきか、そして実務ではどう対策するのかまで紹介します。
Pythonの丸め誤差とは?¶
まずは、Pythonの丸め誤差が何なのかを整理しましょう。
丸め誤差とは、数値をコンピューターで扱うときに、正確な値をそのまま表現できず、近い値に置き換えられることで生じる誤差です。
たとえば、人間にとって 0.1 は単純な小数です。
しかし、コンピューター内部では多くの場合、数値を2進数で表します。そのため、10進数ではきれいに見える 0.1 や 0.2 が、内部では正確に表現できないことがあります。
Python公式ドキュメントでも、多くの10進小数は2進小数として正確に表現できないため、期待した表示にならないことがあると説明されています。
実際に、Pythonで次のコードを実行してみましょう。
print(0.1 + 0.2)
結果は、次のようになります。
0.30000000000000004
本来なら 0.3 になってほしいところです。
しかし、内部的にはわずかなズレを含んだ値として計算されているため、このような結果になります。
ここで大切なのは、Pythonが計算を間違えているわけではないということです。
Pythonは、コンピューターが扱える形式で小数を表現し、その結果を表示しています。
なぜPythonで丸め誤差が起きるのか¶
Pythonの丸め誤差を理解するには、小数がコンピューターの中でどのように保存されるかを知る必要があります。
少し難しく感じるかもしれませんが、イメージで理解すれば大丈夫です。
コンピューターは2進数で数値を扱う¶
私たちは普段、10進数を使っています。
10進数は、0から9までの数字を使って数を表す方法です。一方、コンピューターは基本的に0と1を使う2進数で数値を扱います。
整数であれば、2進数でも比較的わかりやすく表現できます。
しかし、小数になると話が少し複雑になります。
たとえば、10進数の 0.5 は2進数でもきれいに表現できます。
なぜなら、0.5 は2分の1だからです。
一方で、0.1 は2進数で正確に表すことができません。
10進数では短く書けるのに、2進数では割り切れない小数になってしまうのです。
これは、私たちが3分の1を10進数で書こうとすると 0.333333... と無限に続いてしまうのに似ています。
つまり、Pythonのfloat型では、0.1 を正確な 0.1 として持っているのではなく、限りなく近い値として持っています。
float型は近似値を扱っている¶
Pythonで小数をそのまま書くと、多くの場合は float 型として扱われます。
float は浮動小数点数と呼ばれる型です。
浮動小数点数は、幅広い範囲の数値を効率よく扱える便利な仕組みです。
その一方で、すべての小数を正確に表現できるわけではありません。
Python公式ドキュメントでも、floatの演算は10進演算ではなく、各演算で新しい丸め誤差が発生する可能性があると説明されています。
ここは、初心者がつまずきやすいポイントです。
Pythonの float は小数を扱える型ですが、正確な小数計算を保証する型ではありません。
Pythonでよく見る丸め誤差の例¶
ここからは、実際にPythonでよく見る丸め誤差の例を見ていきましょう。
コードを見たほうが、感覚的に理解しやすいはずです。
0.1 + 0.2 が 0.3 にならない¶
もっとも有名な例が、0.1 + 0.2 です。
result = 0.1 + 0.2
print(result)
print(result == 0.3)
実行結果は次のようになります。
0.30000000000000004
False
見た目では、ほぼ 0.3 です。
でも、厳密には 0.3 とは違う値なので、比較すると False になります。
このようなコードは、実務でも意外と危険です。
たとえば、合計金額が指定金額と一致しているかを == で比較していると、想定外の判定になることがあります。
round関数でも期待通りにならないことがある¶
Pythonには round() という丸め用の関数があります。
しかし、round() を使えば常に直感通りになるわけではありません。
たとえば、次のコードを見てください。
print(round(2.675, 2))
結果は次のようになります。
2.67
多くの人は、2.68 を期待するのではないでしょうか。
しかし、Pythonでは 2.67 になります。
Python公式ドキュメントでも、round(2.675, 2) が 2.67 になる例が紹介されており、これはバグではなく、ほとんどの10進小数をfloatで正確に表現できないことが原因だと説明されています。
つまり、round() の問題というより、丸める前の値がすでに少しズレているのです。
表示だけ丸めても内部の値は変わらない¶
初心者のうちは、表示上の丸めと値そのものの丸めを混同しがちです。
たとえば、次のコードを見てみましょう。
value = 0.1 + 0.2
print(f"{value:.2f}")
print(value)
実行結果は次のようになります。
0.30
0.30000000000000004
f"{value:.2f}" を使うと、表示は 0.30 になります。
しかし、変数 value の中身自体が完全な 0.30 に変わったわけではありません。
表示だけ整えたいのか、計算値として丸めたいのか。
ここを分けて考えることが、Pythonの丸め誤差対策ではとても大切です。
Pythonの丸め誤差で注意すべき場面¶
丸め誤差は、すべての場面で大きな問題になるわけではありません。
ただし、影響が出やすい場面を知っておくと、バグを未然に防ぎやすくなります。
次の表に、注意すべき代表例をまとめます。
| 場面 | 起きやすい問題 | 対策の考え方 |
|---|---|---|
| 金額計算 | 1円未満のズレや合計不一致 | Decimalを使う |
| テストコード | 期待値との比較でFalseになる | 許容誤差を使う |
| 集計処理 | 合計値が微妙にズレる | 最後に丸める |
| CSVやExcel連携 | 表示値と内部値が違う | 出力時の桁数を制御する |
| 科学技術計算 | 小さな誤差が積み重なる | 誤差を前提に設計する |
表を見るとわかるように、丸め誤差そのものを完全になくすというより、用途に合わせて扱い方を変えることが重要です。
特に金額計算では、floatをそのまま使うのは避けたほうが安全です。
Pythonの丸め誤差への基本的な対策¶
では、Pythonで丸め誤差を避けるにはどうすればよいのでしょうか。
ここからは、実務でも使いやすい対策を紹介します。
小数を==で比較しない¶
まず大切なのは、小数を == で直接比較しないことです。
次のようなコードは、初心者がよく書いてしまいます。
total = 0.1 + 0.2
if total == 0.3:
print("一致しました")
else:
print("一致しません")
結果は、次のようになります。
一致しません
ほぼ同じ値なのに、一致しないと判定されます。
このような場合は、差が十分に小さいかどうかで比較します。
total = 0.1 + 0.2
expected = 0.3
if abs(total - expected) < 0.000001:
print("ほぼ一致しました")
else:
print("一致しません")
実行結果は次のようになります。
ほぼ一致しました
このように、許容できる誤差の範囲を決めて比較します。
実務では、どのくらいの誤差を許すかを仕様として決めることが重要です。
math.iscloseを使う¶
Pythonには、小数の近似比較に使える math.isclose() があります。
自分で abs() を書くより、意図がわかりやすくなります。
import math
total = 0.1 + 0.2
print(math.isclose(total, 0.3))
実行結果は次のようになります。
True
math.isclose() を使うと、この2つの値は十分近いと判定されます。
テストコードでも、近似値を比較したい場合に役立ちます。
ただし、金額計算で何でも isclose() にすればよいわけではありません。
お金の計算では、誤差を許すよりも、そもそも正確に扱う設計にしたほうが安全です。
金額計算ではDecimalを使う¶
Pythonで金額や会計のような正確性が必要な小数計算をするなら、decimal モジュールを検討しましょう。
Decimal を使うと、10進数として小数を扱いやすくなります。
Python公式ドキュメントでも、decimal は正しく丸められた10進浮動小数点演算をサポートし、会計アプリケーションのように厳密な等価性が必要な場面に適していると説明されています。
Decimalの基本的な使い方¶
まずは、Decimal を使った例を見てみましょう。
from decimal import Decimal
result = Decimal("0.1") + Decimal("0.2")
print(result)
print(result == Decimal("0.3"))
実行結果は次のようになります。
0.3
True
float では 0.30000000000000004 になっていた計算が、Decimal では期待通り 0.3 になります。
ここで重要なのは、Decimal("0.1") のように文字列で渡している点です。
次のように、floatをそのまま Decimal に渡すのは避けたほうがよいです。
from decimal import Decimal
value = Decimal(0.1)
print(value)
この場合、すでにfloatとして近似された値をDecimalに変換してしまいます。
つまり、丸め誤差を含んだ値をDecimalに持ち込むことになります。
Decimalでは文字列から作る¶
Decimalを使うときは、基本的に文字列から作ると覚えておくと安全です。
from decimal import Decimal
price = Decimal("1980")
tax_rate = Decimal("0.10")
tax = price * tax_rate
print(tax)
実行結果は次のようになります。
198.00
金額計算では、入力値を文字列として受け取り、Decimalに変換する流れがよく使われます。
Webフォーム、CSV、APIなどから受け取った値も、最初からDecimalに変換しておくと安心です。
round関数の挙動を正しく理解する¶
Python 丸め誤差を調べている人の多くは、round() の結果に違和感を持って検索しているのではないでしょうか。
ここでは、round() の注意点を整理します。
roundは四捨五入とは限らない¶
Pythonの round() は、私たちが学校で習った四捨五入と完全に同じではありません。
Pythonの公式ドキュメントでは、ちょうど中間にある値は偶数側に丸められると説明されています。たとえば、round(0.5) と round(-0.5) はどちらも 0 になり、round(1.5) は 2 になります。
実際に試してみましょう。
print(round(0.5))
print(round(1.5))
print(round(2.5))
print(round(3.5))
実行結果は次のようになります。
0
2
2
4
2.5 が 3 ではなく 2 になるのは、初心者にはかなり意外かもしれません。
これは、偶数丸めと呼ばれる考え方です。
なぜ偶数丸めが使われるのか¶
偶数丸めは、丸めを大量に行ったときの偏りを減らすために使われます。
毎回0.5を切り上げると、全体として値が大きい方向に偏りやすくなります。
一方、偶数丸めでは、上に丸める場合と下に丸める場合が分散されます。
そのため、統計処理や大量の数値処理では都合がよいことがあります。
ただし、一般的な業務システムや請求金額の計算では、四捨五入、切り上げ、切り捨てなどのルールが明確に決まっていることが多いです。
その場合は、仕様に合わせてDecimalの丸めモードを使うのが安全です。
Decimalで四捨五入・切り上げ・切り捨てをする¶
金額計算では、丸めルールを明示することが大切です。
Decimalには、丸め方法を指定できる quantize() があります。
Decimalで四捨五入する¶
次の例では、小数第2位までに丸めています。
from decimal import Decimal, ROUND_HALF_UP
value = Decimal("2.675")
rounded = value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
print(rounded)
実行結果は次のようになります。
2.68
ROUND_HALF_UP は、一般的な四捨五入に近い丸め方です。
業務で小数点以下2桁まで表示したい場合や、税額計算で端数処理をしたい場合に使えます。
Decimalで切り捨てする¶
切り捨てをしたい場合は、ROUND_DOWN を使います。
from decimal import Decimal, ROUND_DOWN
value = Decimal("123.456")
rounded = value.quantize(Decimal("0.01"), rounding=ROUND_DOWN)
print(rounded)
実行結果は次のようになります。
123.45
小数第3位以降が切り捨てられています。
消費税や手数料の計算では、仕様として切り捨てが指定されることがあります。
Decimalで切り上げする¶
切り上げには、ROUND_UP を使います。
from decimal import Decimal, ROUND_UP
value = Decimal("123.451")
rounded = value.quantize(Decimal("0.01"), rounding=ROUND_UP)
print(rounded)
実行結果は次のようになります。
123.46
このように、Decimalを使うと丸めルールをコード上で明確にできます。
あとからコードを読む人にも、意図が伝わりやすくなります。
実務でよくある丸め誤差の失敗例¶
ここからは、エンジニア歴10年の実務経験をもとに、Pythonの丸め誤差で起きやすい失敗を紹介します。
どれも小さなズレに見えますが、放置すると原因調査に時間を取られます。
合計金額が画面表示と一致しない¶
実務でよくあるのが、明細の合計と画面表示の合計が一致しないケースです。
たとえば、商品ごとに税額を計算して丸めるのか、合計金額に対して税率をかけて丸めるのかで結果が変わることがあります。
from decimal import Decimal, ROUND_DOWN
prices = [Decimal("99"), Decimal("199"), Decimal("299")]
tax_rate = Decimal("0.10")
tax_each = [
(price * tax_rate).quantize(Decimal("1"), rounding=ROUND_DOWN)
for price in prices
]
print(tax_each)
print(sum(tax_each))
このような処理では、どのタイミングで丸めるかが重要です。
明細ごとに丸めるのか、合計後に丸めるのかは、仕様として決める必要があります。
私が過去に担当した案件でも、計算ロジック自体は間違っていないのに、丸めるタイミングの認識がチーム内でズレていて不具合になったことがありました。
仕様書には税率しか書かれておらず、端数処理のタイミングが明記されていなかったのです。
テストで期待値が一致しない¶
Pythonのテストコードでも、丸め誤差はよく問題になります。
たとえば、次のようなテストは失敗します。
def test_total():
total = 0.1 + 0.2
assert total == 0.3
このテストは、人間の感覚では正しそうに見えます。
しかし、Pythonでは 0.1 + 0.2 が厳密な 0.3 ではないため失敗します。
この場合は、近似比較を使います。
import math
def test_total():
total = 0.1 + 0.2
assert math.isclose(total, 0.3)
テストが落ちたときに、ロジックの問題なのか、丸め誤差の問題なのかを切り分けるのは意外と大変です。
だからこそ、小数を扱うテストでは最初から比較方法を意識しておくと安心です。
CSV出力で桁数がバラバラになる¶
データ処理では、計算結果をCSVに出力することがあります。
そのとき、丸め誤差を含んだ値をそのまま出力すると、見た目が不自然になります。
values = [0.1 + 0.2, 1.1 + 2.2, 3.3 + 4.4]
for value in values:
print(value)
結果は、次のようになることがあります。
0.30000000000000004
3.3000000000000003
7.7
このままCSVに出すと、利用者からこの数字は何ですか?と聞かれる可能性があります。
表示や出力が目的なら、桁数を整える処理を入れましょう。
values = [0.1 + 0.2, 1.1 + 2.2, 3.3 + 4.4]
for value in values:
print(f"{value:.2f}")
実行結果は次のようになります。
0.30
3.30
7.70
計算の正確性と、表示の見やすさは別の問題です。
出力時には、ユーザーが見て自然な形式に整えることも大切です。
floatとDecimalの使い分け¶
ここまで読むと、すべてDecimalを使えばよいのでは?と思うかもしれません。
しかし、実務ではfloatとDecimalを使い分けるのが現実的です。
floatは高速で、科学技術計算やグラフ描画、機械学習、統計処理などで広く使われます。
一方、Decimalは正確な10進計算に向いていますが、floatより扱いが少し重くなります。
使い分けの目安を表にまとめます。
| 用途 | おすすめの型 | 理由 |
|---|---|---|
| 金額計算 | Decimal | 10進数として正確に扱いやすい |
| 税率や手数料計算 | Decimal | 端数処理を明示しやすい |
| 科学技術計算 | float | 高速でライブラリとの相性がよい |
| 機械学習 | float | NumPyなどがfloat前提で動くことが多い |
| 表示用の桁揃え | float + format | 表示だけならフォーマットで十分 |
| 厳密な等価比較 | Decimalまたは整数 | floatの直接比較を避けたい |
大切なのは、floatが悪いわけではないということです。
floatは便利で高速です。ただし、正確な10進小数が必要な場面には向いていません。
Python 丸め誤差を防ぐ実務的な考え方¶
Pythonの丸め誤差は、知識だけでなく設計の問題でもあります。
ここでは、実務で意識している考え方を紹介します。
最初に数値の種類を決める¶
実務では、コードを書く前に、この値は何を表しているのかを考えます。
金額なのか、割合なのか、測定値なのか、表示用なのか。
金額であればDecimalを使うことが多いです。
一方、センサー値や統計値なら、多少の誤差を前提としてfloatを使うこともあります。
数値の意味を考えずに、とりあえずfloatで実装すると、後から修正が大変になります。
特に、システム全体に小数計算が広がったあとでDecimalに変えるのは手間がかかります。
丸めるタイミングを決める¶
丸め誤差対策では、どこで丸めるかも重要です。
途中の計算で何度も丸めると、誤差や端数処理の影響が積み重なることがあります。
一方で、最後まで丸めないと、表示や保存のタイミングで想定外の値が出ることもあります。
実務では、計算途中では必要以上に丸めず、保存時や表示時、請求確定時などのタイミングで丸めることが多いです。
ただし、税計算のように法律や業務ルールが関わる場合は、そのルールを優先します。
仕様書に端数処理を書く¶
丸め誤差は、コードだけの問題ではありません。
仕様の曖昧さが原因でバグになることも多いです。
たとえば、次のような内容は仕様書に明記したほうがよいです。
- 小数第何位まで扱うのか
- 四捨五入、切り上げ、切り捨てのどれを使うのか
- 明細ごとに丸めるのか、合計後に丸めるのか
箇条書きは最小限にしましたが、この3つは本当に重要です。
エンジニア歴10年の経験上、丸め誤差の不具合は、実装ミスよりも認識ズレから起きることが多いです。
Pythonの丸め誤差を初心者が学ぶときのポイント¶
初心者がPythonの丸め誤差を学ぶときは、難しい理論から入るより、よくある現象から理解するのがおすすめです。
まずは、0.1 + 0.2 が 0.3 にならない理由をざっくり理解しましょう。
完璧に理解しようとしすぎない¶
浮動小数点数の内部表現を完全に理解しようとすると、かなり深い話になります。
IEEE 754、仮数部、指数部、2進小数など、専門用語も増えていきます。
もちろん、深く理解することには価値があります。
しかし、初心者の段階では、まずは小数は少しズレることがあると覚えるだけでも十分です。
そこから、必要に応じてDecimalや近似比較を学んでいけば大丈夫です。
エラーではなく仕様として受け止める¶
丸め誤差を見ると、最初は不安になるかもしれません。
でも、これはPythonだけの特殊な問題ではありません。
C、Java、JavaScriptなど、多くのプログラミング言語でも同じような現象が起こります。
Python公式ドキュメントでも、Pythonだけでなく多くの言語で、2進浮動小数点数の性質によって期待通りの10進表示にならないことがあると説明されています。
つまり、丸め誤差は避けるべきバグというより、理解して付き合うべき仕様です。
まとめ¶
Pythonの丸め誤差は、初心者にとって少し不思議に見える現象です。
0.1 + 0.2 が 0.30000000000000004 になると、最初は驚くと思います。
しかし、理由はシンプルです。
Pythonのfloatは、多くの小数を2進数の近似値として扱っているため、わずかなズレが生じることがあります。
大切なのは、丸め誤差をなくそうとすることではありません。
用途に合わせて、float、Decimal、format、math.iscloseを使い分けることです。
金額計算ならDecimalを使う。
小数の比較なら == を避ける。
表示だけならフォーマットで整える。
このあたりを押さえておけば、Python 丸め誤差で大きく悩むことはかなり減ります。
Pythonで小数を扱うときは、ぜひ今回の内容を思い出してください。
小さな誤差に気づけるようになると、コードの信頼性は一段上がります。
ここまでお読みいただきありがとうございました。