検索
連載

[Pythonクイズ]「1.0 + 2.0 == 3.0」は期待通りにTrueになるはず? その理由は分かる?Pythonステップアップクイズ

普段何気なく使っている浮動小数点数値ですが、ときには思わぬ結果を生むことがあります。その代表例が今回の問題です。どっちのメッセージが表示されるか分かってますよね?

PC用表示 関連情報
Share
Tweet
LINE
Hatena
「Pythonステップアップクイズ」のインデックス

連載目次

どっちのprint関数が呼び出されるかな?
どっちのprint関数が呼び出されるかな?

【問題】

 以下のPythonコードでは、浮動小数点数値の0.1と0.2を加算した結果は、浮動小数点数値の0.3と比較して、両者が等しいかどうかによって出力するメッセージを切り替えている。どちらのメッセージが表示されるだろう。

a = 0.1 + 0.2
if a == 0.3:
    print('a == 0.3')
else:
    print('a != 0.3')

どっちのprint関数が呼び出されるかな?


かわさき

 どうもHPかわさきです。

 このアカウントは実は筆者個人が日常的に使用しているアカウントではないため、気が付くのが遅くなりましたが、先日公開した「Python特有? こんな書き方できるって知ってましたか?」についてメンションをいただいていました(初めて! 掲載許可など頂いていないので名前は伏せますね)。

 曰く「print('OK' if 0 <= num < 10 else 'NG')」と書けるよと。確かに! なんで、原稿を書いている時に思い付かなかったんでしょう。ご指摘ありがとうございました! 1行で全てが終わるのは気持ちイイですよね。これからもよろしくお願いいたします。

 そういうわけで、これまではいろいろと正解がありましたが、今回は2択問題なのでさすがに答えはどちらかです。よかったよかった。


【答え】

 正解は『「a != 0.3」が表示される』です。

「a != 0.3」が表示される!
「a != 0.3」が表示される!

 Pythonでは(というか、IEEE 754で定められている浮動小数点数値の表現方法では)0.1と0.2、0.3という値を誤差なしで表現することができません。上の画像に示したように、「0.1+0.2」という加算の結果は「0.3000……044」という値になり、一方単純に「0.3」と記した時には、実際にはその値は「0.2999……989」となっています(小数点以下12桁での表記)。そのため、「0.1+0.2」と「0.3」は異なる値として判断されてしまうのです。

 こうした振る舞いが気になる人は、mathモジュールのisclose関数を覚えておくとよいかもしれません。

from math import isclose

a = 0.1 + 0.2
if isclose(a, 0.3):
    print('a is close to 0.3')
else:
    print('a is not close to  0.3')

isclose関数は2つの値が近ければTrueを返す

 isclose関数は2つの値が近ければTrueを、そうでなければFalseを返します。「2つの値が近いかどうか」は相対誤差(デフォルト値:1.0×10-9)、絶対誤差(デフォルト値:0.0)、2つの値の差を基に計算して求めるようになっています(計算式は省略)。上のコードは「a is close to 0.3」と出力します。また、許容範囲なども指定可能ですが、これについては上でリンクしている公式のドキュメントを参照してください。

 上で見たような想定とは異なる振る舞いを避けるためにも、浮動小数点数値の等価比較には「==」演算子を使うのではなく、isclose関数などを使うのがオススメです(NumPyにもisclose関数が用意されています)。

【解説】


かわさき

 以下はそれほど浮動小数点数値やIEEE 754について深堀りできていません。実数の2進表記ってどんなんだっけ? と思う方はちょっと読んでみてもらえるとうれしいです。


 2進数として表現した実数の小数点以下は各桁が1/2、1/4、1/8……のように「1/(2のべき乗)」を表します。そして、各桁は0か1の値を取り、1/(2のべき乗)にそれらを乗じた値がその桁の値となります。例えば、2進数で表現した実数「1010.1011」があったとしましょう。

 この整数部の計算は「23×1+22×0+21×1+20×0」=「8+0+2+0」=「10」となります。小数部の計算は「2-1×1+2-2×0+2-3×1+2-4×1」のような計算になり、その結果は「1/2×1+1/4×0+1/8×1+1/16×1」=「0.5+0+0.125+0.0625」=「0.6875」です。整数部と小数部の計算結果から2進の「1010.1011」は10進の「10.6875」となります。

2進表記の「1010.1011」の計算
2進表記の「1010.1011」の計算

 このように、2進の実数は2xとなる値の和として表現されます。このため、0.1(1/10)や0.2(2/10)、0.3(3/10)のように分数として表現したときに分母に2以外の値が含まれる数値については、小数部は「1/2x(x < 0)」で表される値の和として表現した近似値になってしまうのです。0.1なら「1/2-4+1/2-5+1/2-8+……」=「0.0625+0.03125+0.0078125+……」のようになります。なお、実際には10進数の0.1を2進数にすると「0.00011001100110011……」のような循環小数として表現されます。

 このような実数を浮動小数点数値として表現したり、それらの演算をしたりする方法を定めているのがIEEE 754です。が、ここでIEEE 754に深く立ち入ると泥沼にはまりそうなので、ここでは名前の言及だけに留めておきましょう。

 ただし、実際にIEEE 754に準拠した形で浮動小数点数値がどのように表現されるかだけは確認しておきましょう。Python処理系の多くでは浮動小数点数値は倍精度の値として処理されていることから、浮動小数点数値を64bit幅のビット列(IEEE 754準拠)に変換する関数とその逆を行う関数を以下のように定義しました(簡易版)。

from struct import pack, unpack

def f2b(f):
    tmp = pack('>d', f)
    tmp = unpack('>Q', tmp)[0]
    result = f'{tmp:064b}'
    return result

def b2f(b):
    tmp = int(b, 2)
    tmp = pack('>Q', tmp)
    result = unpack('>d', tmp)[0]
    return result

浮動小数点数値をIEEE 754形式のビット列に変換/逆変換する関数

 f2b関数ではstructモジュールのpack関数とunpack関数を使って、浮動小数点数値を倍精度(8バイト)の浮動小数点数値としてbytes型のオブジェクトに変換(pack)し、それを今度は8バイトの符号なし整数であるかのように変換(unpack)しています(bytesオブジェクトに変換された結果を2進数の形=ビット列として表現できるように変換しています。bytes列がそのままの形で保存されていれば問題ないということです)。b2f関数はその逆をやっていると思ってください。

 これらの関数を使って、0.1+0.2の結果と0.3をビット列に変換してみます。

a = 0.1 + 0.2
b = 0.3
a_bits = f2b(a)
b_bits = f2b(b)
print(a_bits)
print(b_bits)
# 出力結果:
# 0011111111010011001100110011001100110011001100110011001100110100
# 0011111111010011001100110011001100110011001100110011001100110011

0.1+0.2と0.3をIEEE 754に準拠したビット列表現に変換する

 リスト末尾にあるprint関数の出力結果を見ると、最後だけが微妙に異なっていることが分かります。でも、この違いがあるために、両者は異なる値として扱われるということです。なお、IEEE 754形式で表現されるこれらの値(倍精度浮動小数点数値)は先頭のビット(最上位ビット)が符号を、その後に続く11桁が指数部の値を、残りが「1.fraction」のように常にその数値が「1」から始まるように正規化された値の「fraction」部分となります(これ以上の説明はホントにやめておきます)。

 念のため、これらのビット列を浮動小数点数値に戻してみましょう。

a_restored = b2f(a_bits)
b_restored = b2f(b_bits)
print(f'{a:.32f}')
print(f'{a_restored:.32f}')
print(f'{b:.32f}')
print(f'{b_restored:.32f}')
# 出力結果:
# 0.30000000000000004440892098500626
# 0.30000000000000004440892098500626
# 0.29999999999999998889776975374843
# 0.29999999999999998889776975374843

元の値とビット列を浮動小数点数値に復元したものを小数点以下32桁で出力

 このコードでは元の値(0.1+0.2の結果と0.3)および、それらをビット列にしたものから復元した値を小数点以下32桁で出力しています。b2f関数がきちんとビット列を浮動小数点数値に変換できていることが分かりました。


かわさき

 ホントは2つの浮動小数点数値を与えると、それらをビット列に変換して、ビットごとの加算を行って、その結果をまた浮動小数点数値に戻して……なんてところまでやろうと思ってコードを書いていたのですが、どう見てもこのクイズで語ることからは外れちゃうなぁということでボツにしました。というか、繰り上げ処理とかNaNやInfの処理とか、意外にやることが多くてあきらめました。f2b関数やb2f関数はそうしたコードの名残なのです。

 今回は特にリンクしておきたい記事を思い付かなかったので、そっとタブを閉じてくださいませ。


「Pythonステップアップクイズ」のインデックス

Pythonステップアップクイズ

Copyright© Digital Advantage Corp. All Rights Reserved.

[an error occurred while processing this directive]