[Python入門]プログラムは小数が苦手って本当?

プログラムが計算を間違える?

プログラムは小数の取り扱いが苦手というのをご存じでしょうか。
簡単な計算をしてみましょう。

$$ 0.1 + 0.1 + 0.1 = ? $$

答えは\(0.3\)ですね。小数の加算は小学校低学年の単元。特段難しい計算ではありません。

では、計算が得意なはずのプログラムに任せてみましょう。

x = 0.1 + 0.1 + 0.1
print(x)
# 0.30000000000000004

答えは0.30000000000000004です。謎の誤差が発生してしまいました。

小数をうまく保存できない

実はこれは計算がうまくいっていないわけではありません。0.1を正しく保持できていないことが原因です。
format文字列を用いて、0.1をより細かく確認してみましょう。
0.1を小数点以下20桁まで表示すれば本来なら0.10000000000000000000と表示されるはずですが…?

print(f'{0.1:.20f}')
# 0.10000000000000000555

小さな誤差が発生してしまいました。これはコンピュータの数値の扱い方に原因があるのです。

2進数の小数表現

コンピュータは電圧の有無で信号を伝達するため、内部では2進法と呼ばれる数値の表現法を用いています。
電脳世界などというときに、真っ暗な世界の中で0と1が浮かんで流れているイメージが用いられるのもそれが所以です。

この2進法こそが小数をうまく取り扱えない原因なのです。

2進法とは数字を2の指数の和で表現する記法です。
例えば100という整数を2の指数の和で表すと\(100 = 64 + 32 + 4 = 2^6 + 2^5 + 2^2 \)となります。
したがって、2進数表記すると下から7桁目、6桁目、3桁目に1が立って、\(1100100\)となります。

では、0.1を表そうとするとどうなるでしょう。
\(0.1 = 0.0625 + 0.03125 + 0.00390625 + 0.001953125 +…\)

実は、項をいくら増やしても0.1になることはありません。
有限個の項で表すことができないので、0.1ぴったりをデータとして保持できないのです。

なぜ表すことができないの?

注意:数学的な証明なので、数学が苦手な方はこの章を読み飛ばしても構いません。

仮に0.1がn桁の2進数表記で表すことができたとします。したがって以下の等式が成り立ちます。
このとき、\(b_{1} ~ b_{n}\)は各位の数である0または1を表します。

$$ 0.1 = \frac{b_{1}}{2^1} + \frac{b_{2}}{2^2} + \frac{b_{3}}{2^2} + \frac{b_{4}}{2^2} + \frac{b_{5}}{2^2} + … + \frac{b_{n-1}}{2^{n-1}} + \frac{b_{n}}{2^n}$$

ここで、\(b_{1} \times 2^{n-1} + b_{2} \times 2^{n-2} + … + b_{n} = m\)とすると

$$ 0.1 = \frac{m}{2^n}$$

と表せます。また\(0.1 = \frac{1}{10} \)であるため

$$ \frac{1}{10} = \frac{m}{2^n}$$

ここで、両辺に\( 2^n \)をかけると、

$$ \frac{2^{n-1}}{5} = m$$

mは整数であるため、\(2^{n-1}\)は約数に5を含む必要があります。
よって矛盾が生じ「0.1をn桁の2進数として記すことができる」という仮説が否定されます。

したがって、0.1を有限桁の2進数としてあらわすことができないということが証明されます。


もちろん全ての小数が2進数で綺麗に保持できないわけではありません。
例えば0.5(2進数表記: 0.1)や0.75(2進数表記: 0.11)などのような数値に関しては正しく保持・計算することができます。

print(f'{0.75:.20f}')
# 0.75000000000000000000

誤差があるといえども微々たるものですし、小数の比較などが存在しない限りは普段意識するようなことはあまりないかもしれません。
しかし、バグの原因になりかねない事実であるため、覚えておいて損はなしです!

>Python TIPSのほかの記事を読む
>開発者ブログのほかの記事を読む

ライター:H.I