x
に0.1
を代入し、コンソールに表示すると0.1
と表示されます。
x = 0.1
print(x)
# => 0.1
当たり前のことに感じますが、0.1
は浮動小数点数(IEEE 754)では正確に表現できません。
なのにprint
をしたときに0.1
と表示されるのは不思議です。
このことについて分かったことを書いておきます。
環境
この記事ではPython 3.7を使用しています。
【前提】浮動小数点数
この記事で、以降"浮動小数点数"という場合は、"IEEE 754 倍精度"のことを指します。
浮動小数点数のフォーマットは、数を以下の形式に変換し、sign
、exp
、frac
を順に並べたものです。
(-1)^{sign} \times 2^{exp - 1023} \times (1 + frac \times 2^{-52})
それぞれの記号の名前と範囲は以下の通りになります。
記号 | 日本語名 | 英語名 | 範囲 |
---|---|---|---|
sign | 符号 | sign | 0か1 |
exp | 指数 | exponent | 1から2,046 |
frac | 仮数 | fraction | 0から4,503,599,627,370,495 |
10進数をこの形式に変換する方法は、こちらを参照してください。
http://www.picfun.com/mathlib02.html
10進数の小数を2進数の小数にする方法はこちらがわかりやすいです。
https://mathwords.net/syosuu2sin
多くの場合、10進数の小数は2進数では循環小数となります。1
10進数の小数を2進数に変換する過程で、有限桁で操作を打ち切った場合、変換前の数と誤差を生じます(丸め誤差)。
例えば0.1を浮動小数点数で表現したとき
0.1を浮動小数点数に変換します。
変換には、以下のツールを利用しました。
https://tools.m-bsys.com/calculators/ieee754.php
0.1
は、浮動小数点数の2進数表現では、
符号 = 0
指数 = 01111111011
仮数 = 1001100110011001100110011001100110011001100110011010
10進数に変換すると、
符号 = 0
指数 = 1019
仮数 = 2702159776422298
です。
上記の浮動小数点数の式に当てはめると、
\begin{align}
&(-1)^{0} \times 2^{1019-1023} \times (1 + 2702159776422298 \times 2^{-52})\\
&= 1 \times 2^{-4} \times (2^{52} \times 2^{-52} + 2702159776422298 \times 2^{-52})\\
&= 2^{-4} \times (4503599627370496 \times 2^{-52} + 2702159776422298 \times 2^{-52})\\
&= 2^{-4} \times 7205759403792794 \times 2^{-52}\\
&= 7205759403792794 \times 2^{-56}\\
&= \frac{7205759403792794}{72057594037927936}\\
&= 0.1000000000000000055511151231257827021181583404541015625
\end{align}
となり、コンピュータ上で0.1は
0.1000000000000000055511151231257827021181583404541015625
として扱われていることがわかります。
これが、例えば0.1
を3回足したときに0.3
ぴったりにならない原因です(正確にいうと、"ならない"というより"表示されない")。
print(0.1 + 0.1 + 0.1)
# => 0.30000000000000004
printした時の挙動
本題の、0.1
が浮動小数点数で正確に表せないのに、print
したときに0.1
と表示されるのはなぜか、ですが、Pythonの公式ページに答えが書いてあります。
昔の Python は、プロンプトと repr() ビルトイン関数は 17 桁の有効数字を持つ 0.10000000000000001 のような10進数の値を選んで表示していました。 Python 3.1 からは、ほとんどの場面で 0.1 のような最も短い桁数の10進数の値を選ぶようになりました。
https://docs.python.org/ja/3/tutorial/floatingpoint.html
とのことです。
つまり、浮動小数点数で、0.1
と同じ表現になる数のうち、一番短い表現が選択されるということです。Python以外の言語でも同じような理由で (訂正:Python以外の言語につきましては、各言語の公式ドキュメントをご確認ください)0.1
が0.1
と表示されるのだと思われます。
逆に言えば、例えば0.1
に非常に近い数であれば、0.1
と表示されます。
print(0.1000000000000000056)
# => 0.1
なお、調べた限りでは、
0.099999999999999998612221219218554324470460414886474609375
から
0.100000000000000012490009027033011079765856266021728515625
までが、print
したときに0.1
と表示される範囲です。この範囲にある数は、0.1
と同じ浮動小数点数に変換されるためです。
1番短い表現が特にない場合は17桁の有効数字を持つ数値を表示するようです。
print(0.12345678901234567890)
#=> 0.12345678901234568
おまけ
Pythonで小数点以下の桁数を指定するには以下のようにします。入力した数値と出力に誤差があることが確認できます。
print(f'{0.1:.20f}')
# => 0.10000000000000000555
続編を書きました
さて、
これが、例えば
0.1
を3回足したときに0.3
ぴったりにならない原因です(正確にいうと、"ならない"というより"表示されない")。
print(0.1 + 0.1 + 0.1)
# => 0.30000000000000004
と書きました。
0.1
の浮動小数点数を10進数で表したものでは18桁目に初めて5
が出るのですが、0.1
を3回足すと17桁目に4
が現れます。
これでは計算が合いません。
以下の記事でこの現象を説明しました。
-
例えば、0より大きく1より小さい数で、小数点以下3桁以内で表される数(999個ある)のうち、2進数に変換した時に循環小数にならないのはわずか7個です。 ↩