プログラムの間違いを探す方法
デバッグとは、プログラムが間違った動きをする理由を見つけて、直すことです。大変なのは、直すコードを打つことではありません。百か所ある中で、どこを見るかを決めることです。
当てずっぽうにコードを変えると、午後が丸ごと消えることがあります。一度に1つずつ確かめれば、数分で済みます。
このレッスンのゴール
メモ
エラーを読み、一度に1つずつ確かめて、バグを見つけられる。
重要な言葉
| ことば | いみ |
|---|---|
| バグ | プログラムを間違った動きにさせる欠陥 |
| 再現 | 同じ間違いを、好きなときに起こすこと |
| トレースバック | エラーで止まったときに Python が出す、呼び出しの一覧 |
| 仮説 | 1回の確認で間違いとわかる、原因の予想 |
| 表示で確認 | 値を画面に出して、中身を見ること |
| 半分に絞る | 残りの真ん中を調べて、毎回半分を除くこと |
学生が入力するセルは、このプレビューでは空白です。
まず、好きなときに起こす
見えない間違いは直せません。何かを変える前に、毎回間違いが起こる入力を見つけて、取っておきます。
小さなプログラムが、ファイルの売上を平均します。1行が1日分です。データには穴があります。先にフォルダーとファイルを作りましょう。
レッスン用のフォルダーを作る
mkdir -p ~/debugtestここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
フォルダーに入る
cd ~/debugtestここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
売上ファイルを書く
printf '月 120\n火 80\n水 なし\n木 200\n' > sales.txtここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
レポートのプログラムを書く
printf 'def parse(line):\n parts = line.split()\n return [parts[0], int(parts[1])]\n\n\ndef average(lines):\n result = 0\n for line in lines:\n pair = parse(line)\n result = result + pair[1]\n return result / len(lines)\n\n\nprint(average(open("sales.txt").read().splitlines()))\n' > report.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
実行して、止まるのを見る
python3 report.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
トレースバックは下から読む
トレースバックは、プログラムが止まったときに開いていた呼び出しの一覧です。いちばん新しい呼び出しが下にあるので、下から読みます。
最後の行は、何が起きたかを言います。ValueError という名前とメッセージです。その上の File の行は、場所を言います。ファイル、行番号、関数名です。さらに上の行は、そこに来た道筋です。
File の行だけを出す
python3 report.py 2>&1 | grep "File"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
1つ確かめて、次へ進む
エラーが示すのは、プログラムが止まった場所です。データが間違っていた理由ではありません。そこで仮説を立てます。1回の確認で間違いとわかる予想を、1つ立てるのです。
今回の仮説は、データのどこかの行が数ではない、です。ファイルを検索して確かめます。何も出なければ仮説は外れですが、それも前進です。
データの中を検索する
grep -n "なし" sales.txtここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
手早い修正は次のバグを隠す
3行目は 水 なし です。その日は売上の数がありませんでした。いちばん手早い直し方は、0として数えることです。試して、もう一度実行しましょう。
数のない日を0として数える
sed -i 's| return \[parts\[0\], int(parts\[1\])\]| if parts[1] == "なし":\n return [parts[0], 0]\n return [parts[0], int(parts[1])]|' report.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
もう一度実行する
python3 report.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
エラーがなくてもバグはある
プログラムは動いて、100.0 を表示しました。しかし、数のある3日分、120、80、200 の平均は 133.3 です。プログラムは間違っているのに、何も言いません。
もう読めるトレースバックはありません。足し算をしている場所に表示で確認を入れて、実際の中身を見ます。
足すたびに、その日を表示する
sed -i 's| result = result + pair\[1\]| print(pair[0], pair[1])\n result = result + pair[1]|' report.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
実行して、1行ずつ読む
python3 report.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
段階が多いときは半分に絞る
このプログラムには4つの段階があります。まず真ん中を調べます。合計が正しければ、間違いはその後ろです。間違っていれば、前です。1回の確認で半分が消えます。
チームは、この方法で、千個の変更の中から機能を壊した1つを見つけます。
例: 段階を順に確かめる
| 段階 | 本来の中身 | 実際の中身 | 判定 |
|---|---|---|---|
| ファイルを読む | 4行 | 4行 | 問題なし |
| 金額を読む | 120, 80, なし, 200 | 120, 80, 0, 200 | 0は作り話の数 |
| 足し合わせる | 400 | 400 | 問題なし |
| 数えた日数で割る | 400 / 3 = 133.3 | 400 / 4 = 100.0 | 数のない日も数えた |
学生が入力するセルは、このプレビューでは空白です。
直したプログラムを書き直す
printf 'def parse(line):\n parts = line.split()\n if not parts[1].isdigit():\n return None\n return [parts[0], int(parts[1])]\n\n\ndef average(lines):\n result = 0\n count = 0\n for line in lines:\n pair = parse(line)\n if pair is not None:\n result = result + pair[1]\n count = count + 1\n return result / count\n\n\nprint(round(average(open("sales.txt").read().splitlines()), 1))\n' > report.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
直したプログラムを実行する
python3 report.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
よくある間違い
注意
原因がわかる前にコードを変え、うまくいかないと別の所を変える人がいます。5回変えると、どれが効いたのかわかりません。先に確かめ、1つだけ変えて、また実行します。
さらに: 壊した変更を探す
チームは、すべての変更を Git に残します。バグが出たとき、古い版は良く、いちばん新しい版は悪いとわかっています。git bisect は、真ん中の版を試すよう求め、変更が1つになるまで履歴を半分にします。
1000個の変更で、手順を書きましょう。試す版は、多くて何個でしょうか。