見出し画像

平方根を求める計算方法を変えてみたらバグっていたというお話

こちらの記事で Akioさんからバグを報告いただいた。

バグ報告というのは本当に有難い。
動かして確認してバグを見つけてくれる人なんてそんなにたくさんはいない。いつもいつも感謝です。


不具合内容

不具合のあったコードの記事はこちら。

不具合の内容そのものは簡単である。

5の平方根を計算すると NoneType エラーが発生する

コードと実行結果を再掲してみる。

コード

def root_digit(square, first, step):
    r = first
    for i in range(1, 10, 1):
        if square < ((r + step) * (r + step)):
            return r
        r += step

def root(square):
    r = 0
    step = 1
    for i in range(1, 10, 1):
        r = root_digit(square, r, step)
        step = step / 10
        print('{:.15f}'.format(r))
    return r

root5 = root(5)
print('{:.15f}'.format(root5))

実行結果

2.000000000000000
2.200000000000000
2.230000000000000
2.235999999999999
2.235999999999999
2.236059999999999
2.236067000000000
Traceback (most recent call last):
  File "/data/data/com.termux/files/home/python/kyoto/root-10-1.py", line 17, in <module>
    root5 = root(5)
            ^^^^^^^
  File "/data/data/com.termux/files/home/python/kyoto/root-10-1.py", line 14, in root
    print('{:.15f}'.format(r))
          ^^^^^^^^^^^^^^^^^^^
TypeError: unsupported format string passed to NoneType.__format__

√5 は「富士山麓におうむ鳴く」であるから、「2.236067」の次の「9」を算出しようとしてエラーになったようだ。


原因を調べてみる

関数「root_digit」の詳細を出力してみる。

コード

def root_digit(square, first, step):
    r = first
    for i in range(1, 10, 1):
        print('root_digit {0} {1:.15f}'.format(i, r))
        if square < ((r + step) * (r + step)):
            return r
        r += step

上記の太字を追加してみた。
ループ回数とその時の計算値を出力している。
結果はこの通り。
問題の「9」の桁を算出するところだけを抜粋する。

root_digit 1 2.236067000000000
root_digit 2 2.236067100000000
root_digit 3 2.236067200000000
root_digit 4 2.236067300000000
root_digit 5 2.236067400000000
root_digit 6 2.236067499999999
root_digit 7 2.236067599999999
root_digit 8 2.236067699999999
root_digit 9 2.236067799999999
Traceback (most recent call last):
  File "/data/data/com.termux/files/home/python/kyoto/root-10-3.py", line 18, in <module>
    root5 = root(5)
            ^^^^^^^
  File "/data/data/com.termux/files/home/python/kyoto/root-10-3.py", line 15, in root
    print('{:.15f}'.format(r))
          ^^^^^^^^^^^^^^^^^^^
TypeError: unsupported format string passed to NoneType.__format__

ん?
root_digit の後にループカウンタを表示しているんだが、これが次のようになっている。
(以下、太字の部分)

root_digit 1 2.236067000000000
root_digit 2 2.236067100000000
root_digit 3 2.236067200000000
root_digit 4 2.236067300000000
root_digit 5 2.236067400000000
root_digit 6 2.236067499999999
root_digit 7 2.236067599999999
root_digit 8 2.236067699999999
root_digit 9 2.236067799999999

あれ?
9回しか実行されてない。
10回実行しないと0→9にたどりつけない。
なんで?


原因は range の仕様認識違い

あー、もう!
だから言うたやん!
range(1, 10)
で1から9までループするという仕様は間違えやすいって!
なんで1から10じゃないのよ!
(って、誰に言うてんねん😥)

この記事にわざわざ書いたのになぁ。

などと一人で吠えていても始まらない。
とりあえず、次のように「range(0, 10, 1)」に修正した。

def root_digit(square, first, step):
    r = first
    for i in range(0, 10, 1):
        if square < ((r + step) * (r + step)):
            return r
        r += step

でもって実行した。

~/python/kyoto $ python root-10-4.py
root_digit 0 0.000000000000000
root_digit 1 1.000000000000000
root_digit 2 2.000000000000000
2.000000000000000
root_digit 0 2.000000000000000
root_digit 1 2.100000000000000
root_digit 2 2.200000000000000
2.200000000000000
root_digit 0 2.200000000000000
root_digit 1 2.210000000000000
root_digit 2 2.220000000000000
root_digit 3 2.230000000000000
2.230000000000000
root_digit 0 2.230000000000000
root_digit 1 2.230999999999999
root_digit 2 2.231999999999999
root_digit 3 2.232999999999999
root_digit 4 2.233999999999999
root_digit 5 2.234999999999999
root_digit 6 2.235999999999999
2.235999999999999
root_digit 0 2.235999999999999
2.235999999999999
root_digit 0 2.235999999999999
root_digit 1 2.236009999999999
root_digit 2 2.236019999999999
root_digit 3 2.236029999999999
root_digit 4 2.236039999999999
root_digit 5 2.236049999999999
root_digit 6 2.236059999999999
2.236059999999999
root_digit 0 2.236059999999999
root_digit 1 2.236060999999999
root_digit 2 2.236062000000000
root_digit 3 2.236063000000000
root_digit 4 2.236064000000000
root_digit 5 2.236065000000000
root_digit 6 2.236066000000000
root_digit 7 2.236067000000000
2.236067000000000
root_digit 0 2.236067000000000
root_digit 1 2.236067100000000
root_digit 2 2.236067200000000
root_digit 3 2.236067300000000
root_digit 4 2.236067400000000
root_digit 5 2.236067499999999
root_digit 6 2.236067599999999
root_digit 7 2.236067699999999
root_digit 8 2.236067799999999
root_digit 9 2.236067899999999
2.236067899999999
root_digit 0 2.236067899999999
root_digit 1 2.236067909999999
root_digit 2 2.236067919999999
root_digit 3 2.236067929999999
root_digit 4 2.236067939999999
root_digit 5 2.236067949999998
root_digit 6 2.236067959999998
root_digit 7 2.236067969999998
2.236067969999998
2.236067969999998

~/python/kyoto $

んー。
動くには動いたんだが。

2.200000000000000
root_digit 0 2.200000000000000
root_digit 1 2.210000000000000
root_digit 2 2.220000000000000
root_digit 3 2.230000000000000

ここはきれいよね。
0.01ずつ、きれいに足し算できてる。
でも、その次よね。

2.230000000000000
root_digit 0 2.230000000000000
root_digit 1 2.230999999999999
root_digit 2 2.231999999999999
root_digit 3 2.232999999999999
root_digit 4 2.233999999999999
root_digit 5 2.234999999999999
root_digit 6 2.235999999999999

なんだか、汚い。
0.001ずつ足し算するはずなのに
2.230000000000000
の次が、なんで
2.230999999999999
になる。

誤差。
なんだろうな。
だから、小数ってきらい😢

この桁の計算結果は
2.235999999999999
となって
2.236
に足りていない。

次の次の桁で追い付きはしているんだけど。
(次の太字のところ)

2.235999999999999
root_digit 0 2.235999999999999
2.235999999999999
root_digit 0 2.235999999999999
root_digit 1 2.236009999999999
root_digit 2 2.236019999999999
root_digit 3 2.236029999999999
root_digit 4 2.236039999999999
root_digit 5 2.236049999999999
root_digit 6 2.236059999999999


root_digit には戻り値を返さないルートがある

直接的な原因は「range の仕様認識違い」ではあるんだが、root_digitにはもう1つ問題がある。
戻り値を返さないルートがあるのだ。

そもそも NoneType になった理由でもあるんだが。

def root_digit(square, first, step):
    r = first
    for i in range(0, 10, 1):
        if square < ((r + step) * (r + step)):
            return r
        r += step

return は上記の太字の部分だけである。
もし。
もしも、10回で次の条件にヒットしなければどうなるのか。

if square < ((r + step) * (r + step)):

そう。
return 文を通らずに関数は終了するんである。
そして戻り値はNoneTypeと相成る。
ああ…orz。

10回で条件に到達しないことがあり得るのか。
なさそうではある。
あり得るはずがないのでエラーで問題を生起するという考え方もある。そういう考え方もあるが、そうであればエラー条件を明記するべきだろうな。
明記できなければ、必ず数値を返すべきか。

def root_digit(square, first, step):
    r = first
    for i in range(0, 10, 1):
        if square < ((r + step) * (r + step)):
            return r
        r += step
    return r

これで、少なくともNoneTypeはなくなると思われる。
演算の精度は別にして。

ふぅ~。

この記事が気に入ったらサポートをしてみませんか?