0.1 + 0.2 が 0.3 にならないのはなぜ?|PythonのfloatとDecimal、round、math.isclose

Pythonで合計を出すスクリプトを書いて、テストのつもりで 0.1 + 0.2 を表示してみる。すると画面に出てくるのは 0.30000000000000004。

「え、0.3じゃないの?」って、思わず二度見しますよね。

よくあるのが、if文で == 0.3 と比べていて、なぜか条件に入らないパターンです。コードを何度見直しても間違いが見つからない。実は、間違っているのはコードの書き方ではなく、小数の持ち方のほうなんです。

この記事では、私の環境で実際に動かした結果を見せながら、次の順番で進めます。

  1. うまくいかない例を見る
  2. なぜそうなるのかを知る
  3. やりがちだけど直らない例を見る(ここが大事)
  4. 正しいやり方を選ぶ

確認した環境は Debian 13 / Python 3.13.5 です。WindowsとmacOSでは確かめていません。

リールやカルーセルで同じ計算を見た人も、この先は同じ数字と、同じ「やりがちだけど誤差が残る」例が出てきます。


ステップ1:0.1 + 0.2 が 0.3 にならない

まずは定番の失敗例からいきましょう。Pythonを対話モード(python3 と打って、1行ずつ結果を返してもらう使い方)で開いて、次の3つを打ってみます。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
>>> 0.1 + 0.1 + 0.1 == 0.3
False

0.1 + 0.2 と 0.3 を比べた結果(Debian 13 / Python 3.13.5 のターミナル)

0.1 + 0.2 == 0.3 が False。つまり「等しくない」と判定されました。

ここで出てきた 0.1 のような小数は、Pythonでは float(フロート。小数を扱うための型)として扱われます。原因はこのfloatの中身にあります。


ステップ2:画面の 0.1 は「見かけ」だった

中身をのぞいてみる

format(値, '.55f') と書くと、小数点以下55けたまで表示できます。これで0.1の中身をのぞいてみましょう。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
>>> 0.1
0.1
>>> format(0.1, '.55f')
'0.1000000000000000055511151231257827021181583404541015625'
>>> format(0.3, '.55f')
'0.2999999999999999888977697537484345957636833190917968750'

ふつうに 0.1 と打つと 0.1 と表示されます。でも中身は 0.1000000000000000055… と、ほんの少しだけ大きい数でした。

逆に0.3は、0.2999999999999999888… と、ほんの少しだけ小さい数です。

少し大きい0.1と0.2を足すと、0.3より少し大きくなる。それを少し小さい0.3と比べるから、等しくならないんですね。

なぜ 0.1 ぴったりにならないのか

Pythonのfloatは、数を 2進数(0と1だけで数を表すやり方)で持っています。公式ドキュメントでは、多くの環境のfloatが IEEE 754 の2進浮動小数点(binary64。精度は53ビット)だと案内されています。

10進数で 1÷3 を書くと 0.333… と終わらないのと同じで、0.1を2進数で書くと、終わらない小数になります。

floatは、決まったけた数(上の53ビット分)しか覚えておけません。なので、終わらない部分は途中で丸めて、いちばん近い数を覚えておきます。これが 浮動小数点数(小数点の位置を動かしながら、限られたけた数で小数を表す仕組み)のふつうの動きです。

画面に 0.1 と出るのは、Pythonが「この中身なら、0.1と書くのがいちばん短くて元の値に戻せる」と判断して、短く見せてくれているからです。

画面に見える0.1と、floatの中身の比較

いつも同じだけずれる、わけではない

ここで、ちょっと意地悪な実験をしてみました。0.1を10回足して1.0になるか、書き方を変えて比べます。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
>>> total = 0.0
>>> for _ in range(10):
...     total += 0.1
...
>>> total
0.9999999999999999
>>> sum([0.1] * 10)
1.0
>>> 0.1 * 10
1.0

for文で足した場合と、sum・掛け算の場合の比較

for文で1回ずつ足していくと 0.9999999999999999。でも同じ環境で、sum([0.1] * 10) と 0.1 * 10 は 1.0 になりました。

つまり「floatの計算は毎回ずれる」わけではありません。途中で何回丸めが起きたか、どの順番で計算したかで、ずれたり、たまたまぴったりになったりします。

ここがやっかいなところで、「テストでは1.0になったから大丈夫」と思っても、書き方が変わるとずれることがあります。

ここまでで「自分のコードのどこが危ないのか分からない」と感じたら、AIに聞いてみるのも手です。

なお、AIにコードを貼るときは、パスワード、APIキー、個人情報、会社の秘密が入っていないか先に確認してください。その部分は消すか、ダミーの値に置き換えてから貼りましょう。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
Pythonで小数を足したり比べたりしているコードがあります。
float(浮動小数点数)の誤差で、結果がずれたり == の比較が False になったりしそうな箇所を探してください。
それぞれ、なぜ危ないのかと、Decimal や整数を使った直し方の候補を教えてください。

【コード】
(ここに貼る。パスワード・APIキー・個人情報・会社の秘密は消すかダミーにする)

ステップ3:やりがちだけど直らない例

原因が分かると、次は「じゃあこう直せばいいのかな」と手を動かしたくなりますよね。ここでは、やりがちだけど直らない例を2つ、あえて試してみます。

直らない例1:Decimal(0.1) と書いてしまう

Pythonには Decimal(デシマル。10進数のまま小数を正確に扱うための型)があります。「じゃあDecimalにすればいいんだ」と、次のように書くのがよくあるパターンです。

  • 1
  • 2
  • 3
  • 4
  • 5
>>> from decimal import Decimal
>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> Decimal(0.1) + Decimal(0.2) == Decimal('0.3')
False

floatで渡したDecimalと、文字列で渡したDecimalの比較

False のままです。

Decimal(0.1) のカッコの中の 0.1 は、Decimalに渡される前に、もうfloatになっています。Decimalは、そのfloatの中身 0.1000…055… をそのまま正確に受け取ってしまうんです。ずれた値を正確にコピーしているだけなので、直りません。

文字列('0.1' のようにクォートで囲んだもの)で渡すと、結果が変わります。

  • 1
  • 2
  • 3
  • 4
>>> Decimal('0.1')
Decimal('0.1')
>>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
True

今度は True になりました。違いは、カッコの中にクォートがあるかどうかだけです。

直らない例2:round で四捨五入したつもり

もう1つのやりがちが、round() で小数第2位にそろえる書き方です。

  • 1
  • 2
>>> round(2.675, 2)
2.67

2.675を小数第2位にしたら、2.68になると思いますよね。でも結果は 2.67 でした。

中身をのぞくと、理由が見えてきます。

  • 1
  • 2
>>> format(2.675, '.20f')
'2.67499999999999982236'

floatの2.675は、中身が 2.67499… と、2.675より少しだけ小さい数です。2.675ちょうどではないので、小数第2位にすると2.67のほうに寄ります。この例の主な原因は、中身がずれていることです。公式ドキュメントの round の注釈でも、round(2.675, 2) が 2.68 ではなく 2.67 になる例が載っていて、「ほとんどの小数が浮動小数点数で正確に表せないことの結果」であり、バグではない、と書かれています。

round(2.675, 2) とDecimalの quantize の結果

ちなみに、Pythonの round() には 偶数丸め(ちょうど真ん中のときは、結果が偶数になるほうに丸めるルール)もあります。round(2.5) は 2、round(0.5) は 0、round(1.5) は 2 です。

ただ、これは2.675の件とは別の話です。2.675はfloatの中ではそもそも「ちょうど真ん中」ではないので、偶数丸めの出番になる前に、2.67のほうが近いと決まっています。ここを混ぜて「偶数丸めのせい」と覚えると、ほかの数で混乱するので注意してください。

Decimalで丸めるときは、quantize()(けたをそろえるメソッド)を使います。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
>>> from decimal import Decimal, getcontext
>>> getcontext().rounding
'ROUND_HALF_EVEN'
>>> Decimal('2.675').quantize(Decimal('0.01'))
Decimal('2.68')
>>> Decimal(2.675).quantize(Decimal('0.01'))
Decimal('2.67')

文字列で渡した Decimal('2.675') は、ちゃんと 2.68 になりました。floatから作った Decimal(2.675) は、やっぱり 2.67 です。

ここで1つ、細かいけど大事な点があります。Decimalの既定の丸め方は ROUND_HALF_EVEN、つまり偶数丸めです。Decimal('2.675') が 2.68 になったのは、真ん中のときに偶数の8のほうを選んだからです。

なので、たとえば Decimal('2.665') を同じように丸めると、2.67 ではなく 2.66 になります(これも私の環境で確認しました)。学校で習った四捨五入にしたいときは、rounding=ROUND_HALF_UP を指定します。

  • 1
  • 2
  • 3
from decimal import Decimal, ROUND_HALF_UP

Decimal('2.665').quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)  # Decimal('2.67')

どの丸め方を使うべきかは、扱う数字の決まりによって変わります。迷ったら、こんなふうに聞いてみてください。

  • 1
  • 2
  • 3
PythonのDecimalで小数第2位に丸めています。
ROUND_HALF_EVEN(既定)と ROUND_HALF_UP の違いを、具体的な数字の例で説明してください。
私の用途は「(ここに用途。例:アンケートの平均点を表示する)」です。どちらを選ぶときに、何を確認すればいいかも教えてください。

なお、お金の計算の端数処理や、税・会計に関わるルールは、AIの答えだけで決めないでください。最終的な判断は、決まりを確認したうえで人間が行いましょう。


ステップ4:近いかどうかは math.isclose、一致させたいなら Decimal か整数

math.isclose は「近いかどうか」を見る

floatどうしを比べたいときは、math.isclose() が使えます。2つの数が「十分近いか」を判定する関数です。

  • 1
  • 2
  • 3
>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True

True になりました。ただし、ここにも落とし穴があります。

  • 1
  • 2
  • 3
  • 4
>>> math.isclose(0.0, 1e-10)
False
>>> math.isclose(0.0, 1e-10, abs_tol=1e-9)
True

math.isclose の既定の動きと abs_tol の指定

1e-10 は0.0000000001のこと。ほぼ0なのに、0.0と比べると False です。

math.isclose() は、許容誤差(どれくらいの差までなら「同じ」とみなすかの幅)を2種類持っています。

  • rel_tol:比べる数の大きさに対する割合。既定は 1e-09
  • abs_tol:割合ではなく、差そのものの幅。既定は 0.0

0の近くでは「割合」がほとんど働かないので、abs_tol が0.0のままだと、ほんの少しの差でも False になります。公式の math.isclose でも、既定は rel_tol=1e-09、abs_tol=0.0 で、0.0 と比べるときは正の abs_tol を指定するよう案内されています。0付近の値を比べるなら、abs_tol を自分で決めて渡しましょう。

そして、math.isclose() は「だいたい同じか」を見るためのものです。金額がぴったり一致しているかの判定には使わないでください。「近いからOK」で済ませてはいけない計算もあります。

一致させたいなら、Decimalに文字列で渡す

ぴったりの小数で計算したいなら、ステップ3で見たとおり、Decimalに文字列で渡します。

  • 1
  • 2
  • 3
  • 4
  • 5
>>> from decimal import Decimal
>>> Decimal('0.1') + Decimal('0.2')
Decimal('0.3')
>>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
True

CSVやフォームから読み込んだ値は、もともと文字列のことが多いですよね。そのときは、いったんfloatにせず、文字列のまま Decimal() に渡すのがポイントです。

もう1つの手:整数の小さい単位で計算する

小数を使わない、という手もあります。たとえば0.10と0.20を、「100分の1」を1とする単位で、10と20という整数として持っておくやり方です。

  • 1
  • 2
>>> 10 + 20 == 30
True

整数どうしの足し算なら、ずれは起きません。表示するときだけ100で割って小数に戻します(このとき、Decimalで割ると表示までずれずに済みます)。

文字列のDecimal・整数の単位・発展の実験の結果

やりがちだけど直らない例と、正しいやり方の比較


発展:もう少しだけ意地悪な実験

余裕があれば、ここも試してみてください。

大きな数に1を足しても変わらない
  • 1
  • 2
>>> 1e16 + 1 == 1e16
True

1e16 は1のあとに0が16個並ぶ数です。そこに1を足しても、「足す前と同じ」と判定されました。floatが覚えておけるけた数には限りがあるので、大きな数の一番下のけたは、丸められて消えてしまうんです。

Fraction も、渡し方しだい

Fraction(フラクション。分数のまま計算する型)でも、Decimalと同じことが起きます。

  • 1
  • 2
  • 3
  • 4
  • 5
>>> from fractions import Fraction
>>> Fraction(0.1) + Fraction(0.2) == Fraction(0.3)
False
>>> Fraction('0.1') + Fraction('0.2') == Fraction('0.3')
True

floatから作った Fraction(0.1) は、floatの中身をそのまま分数にするので、誤差を引き継ぎます。文字列で渡した Fraction('0.1') なら、ぴったり10分の1です。

型を変えても、入口がfloatならずれはついてくる。これがこの記事でいちばん覚えておいてほしいことです。


つまずきやすい点

症状 原因 対処
0.1 + 0.2 == 0.3 が False floatの中身が0.1や0.3ぴったりではない Decimalに文字列で渡す、または整数の単位で計算する
Decimalにしたのに直らない Decimal(0.1) のようにfloatを渡している Decimal('0.1') と文字列で渡す
round(2.675, 2) が2.67になる floatの2.675の中身が2.67499… Decimal('2.675').quantize(Decimal('0.01')) を使う
Decimalで丸めたら思った値と違う 既定の丸め方が偶数丸め(ROUND_HALF_EVEN) 四捨五入したいなら rounding=ROUND_HALF_UP を指定する
math.isclose(0.0, 1e-10) が False 既定の abs_tol が0.0 0付近では abs_tol を指定する
テストでは合ったのに本番でずれた 計算の書き方や順番で丸めの回数が変わる 一致が必要な計算はfloatを使わない

エラーや結果のずれの原因が分からないときは、こんなふうに相談してみてください。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
Pythonで次の計算をしたところ、期待した結果になりませんでした。
原因の候補と、確認する順番、直し方を教えてください。
float・Decimal・round・math.isclose のどれが関係していそうかも説明してください。

【実行したコード】
(ここに貼る。パスワード・APIキー・個人情報・会社の秘密は消すかダミーにする)

【期待した結果】
(例:0.3)

【実際の結果】
(例:0.30000000000000004)

まとめ

  • 画面に出る 0.1 は見かけで、floatの中身は 0.1000000000000000055…
  • 0.1は2進数で表しきれないので、floatは近い値で覚えている
  • ずれるかどうかは書き方しだい。「いつも同じだけずれる」わけではない
  • Decimal(0.1) や round(2.675, 2) は、やりがちだけど直らない
  • Decimalの既定の丸め方は偶数丸め。四捨五入は ROUND_HALF_UP を指定する
  • math.isclose() は近いかどうかの判定用。0付近では abs_tol を渡す
  • ぴったり一致させたいなら、Decimalに文字列で渡すか、整数の小さい単位で計算する

次の一歩は、今使っているスクリプトの中から == 0. や round( を検索して、floatで比べたり丸めたりしている場所を1つだけ見直してみることです。


関連する記事

より詳しく知りたいときは、Python公式ドキュメントの 浮動小数点演算、その問題と制限、decimal、math.isclose、round も参考になります。