デバッグの方法
デバッグの流れ
デバッグは以下の図のような流れになります。一般的に、「エラーを読む」、「プリントデバッグをする」などの方法を複数組み合わせて、エラーの原因を特定していくことになります。
エラーを読む
エラー文にはデバッグへの手がかりとなる情報が記載されており、エラーを読むことはデバッグへの第一歩です。ただプログラミングを始めたばかりの人には、エラーを読み解くのは難しく、最終的に読まなくなってしまうこともあります。エラーを読み解くのが難しい理由には次のようなものがあります。
しかし、エラーは常に決まった型にしたがって表示されるため、エラーの構造と頻出する英語表現を一度理解してしまえば、エラーを読み解くのはずっと簡単になります。例えば、次のようなPythonのエラーをみてみましょう。
▼ コード
name = "山田"
print(nama)
▼ エラー
Pythonに限らずどんな言語でも、エラーは「エラーの種類」、「エラーメッセージ」、「トレースバック(スタックトレース)」の3つの要素から構成されます。「エラーの種類」と「エラーメッセージ」に頻出する英語表現は決まっていて、基本的なものを以下の表に示します。
Pythonの基本的なエラー一覧
エラーの分類 | エラーの種類 | エラーメッセージ | エラーの意味 |
構文に関するエラー | SyntaxError | invalid syntax | 構文が正しくない |
keyword argument repeated | 関数呼び出しの中で引数が重複して与えられている | ||
unterminated string literal | 文字列で'や"を締め忘れている | ||
Missing parentheses in call to ‘x’ | xを呼び出すのに必要な括弧が不足している | ||
IndentationError | unexpected indent | 不要なインデントがある | |
unindent does not match any outer indentation level | インデントの数が一致しない | ||
expected an indented block | インデントが必要な場所にインデントがない | ||
インポートに関するエラー | ImportError | cannot import name 'x' from 'y' | モジュールにオブジェクトが含まれていないなどの理由により、 モジュールyからオブジェクトxをインポートできない |
ModuleNotFoundError | No module named 'x' | モジュールがインストールされていないなどの理由で、 モジュールxがインポートできない | |
型や値、名前に関するエラー | AttributeError | 'x' object has no attribute 'y' | オブジェクトxに属性やメソッドyが存在しない |
TypeError | 'x' object is not callable | 呼び出し方が間違えていてオブジェクトxが呼び出せない | |
unhashable type | 可変オブジェクトなどをディクショナリーオブジェクトのkeyに入力されている | ||
can only concatenate str (not "int") to str | 異なる型を連結しようとしている | ||
ValueError | could not convert string to float: 'x' | 関数に想定していない値が渡された | |
ZeroDivisionError | division by zero | 0で割り算が行われた | |
NameError | name 'x' is not defined | 変数や関数が定義されていないか参照する名前のスペルを間違えている | |
インデックスやキーに関するエラー | IndexError | list index out of range | リストやタプルなどの範囲外のインデックスが指定されている |
KeyError | 辞書の存在しないキーが指定されている | ||
ファイルやディレクトリに関するエラー | FileNotFoundError | No such file or directory | 指定したファイルやディレクトリが存在しない |
FileExistsError | File exists | 作成しようとしたファイルがすでに存在する |
「トレースバック(スタックトレース)」はエラーが発生するまでの流れを辿ったものであり、処理がどのように進んだかを表す履歴情報です。Pythonの場合、一番下にエラーが発生した最終地点が記載され、その上を見るとどのような順序で処理が実行されたか辿ることができます。
▼ コード
def fn1():
fn2()
def fn2():
fn3()
def fn3():
x
fn1()
▼エラー
エラーが発生した際には、まずどこでエラーが発生したか(最終地点)を確認し、その時点でエラーを解決できるのであればそれより前の履歴情報を読む必要はありません。そのため、上から順にすべての行を読むのではなく、まずは最終地点の1行を読み、エラーが解決できないか検討するようにしましょう。
プリントデバッグをする
例として、配列を引数として受け取り、すべての要素の和を計算する関数calc_sum()について実装する場合を考えてみましょう。以下のように実装しましたが、このコードには不具合があります。
▼コード
def calc_sum(array):
sum = 0
for i in array:
sum = sum + i
return sum
input_array = [1, 2, 3, 4, 5]
result = calc_sum(input_array)
# result: 1
そこで、どこに不具合があるのか原因を探すために以下のようにプリント文を仕込んでみます。
▼コード(プリントデバッグ)
def calc_sum(array):
print(f"(1) array = {array}")
sum = 0
for i in array:
print(f"(2) i = {i}")
sum = sum + i
return sum
input_array = [1, 2, 3, 4, 5]
result = calc_sum(input_array)
print(f"(3) result = {result}")
# result: 1
このコードを実行すると以下の出力が得られます。
▼出力
この出力を見るとfor文の中で最初の要素しか出力されていないことがわかります。そこで、実行したコードを見てみると、for文の中にreturn文があり、配列の最後の要素まで繰り返しが実行されていない事に気づくでしょう。したがって、return文のインデントを減らせば、意図した実行結果が得られるようになります。
▼コード(修正後)
def calc_sum(array):
print(f"(1) array = {array}")
sum = 0
for i in array:
print(f"(2) i = {i}")
sum = sum + i
return sum # ← インデントを減らす
input_array = [1, 2, 3, 4, 5]
result = calc_sum(input_array)
print(f"(3) result = {result}")
# result: 15
▼出力(修正後)
このようにプリントデバッグでは、変数の中身、関数内部の処理、if文やfor文内の処理経路を確認しながら不具合の原因を探っていきます。漠然とコードを眺めているより、プリント文で地道に情報を集めていくほうが、効率的な場合が多いです。
デバッガを使う
デバッガはより効率的にプリントデバッグを行うためのツールで、特定の場所で処理を中断して、その時点での変数の中身を確認したり、新しいコードを実行したりできます。
VSコードでのデバッガの使い方
左端のツールバーの「実行とデバッグ」アイコンから「デバッグの開始」をクリックするとデバッガを利用できます。初回はlaunch.jsonを作成する必要がありますが、今回はデフォルト設定のままでかまいません。
ブレークポイント
ブレークポイントを設定すると、その位置でプログラムの処理を一時停止して、変数の中身などを確認できます。エディター上の該当の行の左端の余白をクリックすると、ブレークポイントを設定できます。
実際、ブレークポイントを設定した状態で「デバッグの開始」を実行すると、ブレークポイントの位置で処理が一時停止します。
また、左側の「v 変数」に、ブレークポイントの位置での変数の値が表示されます。
デバッグコンソールでコードを実行すると、ブレークポイントの位置での変数の値を使って、新しいコードを試すことができます。
上記の操作が確認できたら、画面右上に表示される「続行」ボタンを押して処理を進めるか、「停止」ボタンを押してデバッグを終了します。
最後に、ブレークポイント(赤丸)をクリックすればブレークポイントを解除することができます。
条件つきブレークポイント
for文など何度もブレークポイントの処理が実行される場合、毎回ブレークポイントの位置で処理が停止されてしまいます。ある条件を満たしているときだけ処理を停止したい場合は、ブレークポイントに条件をつけることができます。
ブレークポイント(赤丸)を右クリックし、「ブレークポイントの編集」をクリックします。
条件式に”i==3”と入力します。
Enterを押すと条件つきブレークポイントの設定が完了します。
実際、「デバッグの開始」を実行すると、条件式”i==3”をみたしたときに処理を停止できていることが確認できます。
上記の操作が確認できたら、画面右上に表示される「続行」ボタンを押して処理を進めるか、「停止」ボタンを押してデバッグを終了します。
ステップ実行
ステップ実行は、ブレークポイントで停止した位置から1ステップずつ処理を実行するための機能で、処理の詳細を理解したいときに役立ちます。ステップ実行には、ステップイン、ステップオーバー、ステップアウトの3つがあります。
機能 | 説明 |
ステップイン (Step In) | 現在の行を実行して1行進める。 関数呼び出しがあれば関数の中に入り、 すべてのコードを1行ずつ実行していく。 |
ステップオーバー (Step Over) | 現在の行を実行して1行進める。 関数呼び出しがあっても関数の中には入らず、 関数を実行して次の行に進む。 |
ステップアウト (Step Out) | 現在の関数の終わりまでコードを実行し、 呼び出し元に戻る。 |
各機能に対応するボタンは、「デバッグの開始」を実行すると画面の右上に表示されます。
ネットで調べる
外部ライブラリを利用している時にエラーが出る場合、まずは公式ドキュメントを見て、ライブラリの使い方に誤りがないか確認しましょう。
公式ドキュメントの情報だけではエラーを解決できない場合、エラーメッセージをそのまま検索し、誰かが同じエラーを解決してくれていないか確認しましょう。この時、自分の環境固有の情報(バージョン、ファイル名、ソースコードの行番号など)が含まれないように注意してください。
日本語で調べてもヒットしない場合は、英語でも検索してみましょう。検索結果のURLの末尾に「&lr=-lang_ja」と入力すれば、日本語以外のサイトを対象に検索できます。
AIを活用する
AIを使ったサポートツールも続々と登場してきています。AIに直接エラー文を投げ込むだけでエラーの原因がわかることもあります。ただし、入力した文章はすべて収集されていると考えたほうが良く、シークレットキーや社内の非公開コードなど機密情報の取り扱いには十分に注意してください。
質問する
調べてもエラーの原因が特定できない場合、「Stack Overflow」のようなプログラミングに関するコミュニティに質問することも選択肢の一つになります。質問する場合は回答しやすいように以下の点に注意するようにしましょう。
自分で解決することで身につく力もありますが、15分以上膠着状態が続くような場合は、自分一人で抱え込まずに質問してしまったほうが良いかもしれません。
デバッグのためのTips
少しずつコードを追加する
一度に大量の変更を加えると、どこでバグが入り込んだのかわからなくなる。少しずつコードを変更していくことで、不具合原因の箇所が限定されデバッグが簡単になる。
経路をたどる
プリントデバッグでは、変数の中身を確認するだけでなく、プログラムがどのような経路をたどって実行されているか確認するのに役立ちます。原因箇所を特定するためには、該当の処理がそもそも実行されているか、処理が最後まで実行されているかをちゃんと把握することで、不具合の原因を突き止められることがあります。
▼コード(プリントデバッグ)
def main():
print("main()を実行します")
func1()
print("main()を実行します")
def func1():
print("func1()を実行します")
func2()
print("func1()を実行します")
def func2():
print("func2()を実行します")
# 何かの処理
print("func2()を実行します")
main()
二分探索でエラー箇所を絞り込む
二分探索は、探索範囲を二分割して探索対象の位置を絞り込んでいく探索方法です。以下のような不具合を含むコードをデバッグする場合を考えてみましょう。
▼コード
def main():
a = 0;
b = 1:
c = 2;
d = 3;
e = 4;
f = 5;
g = 6;
h = 7;
main()
以下のように二分探索を利用して半分をコメントアウトして実行することで、エラー箇所をどんどん絞り込むことができます。(もっとも、このコードはエラーメッセージを見るだけで原因箇所を特定できてしまいますが…)
▼コード(二分探索ー1回目)
def main():
# a = 0;
# b = 1:
# c = 2;
# d = 3;
e = 4;
f = 5;
g = 6;
h = 7;
main()
下半分のみではエラーが出ないので、上半分でエラーが発生しているようです。
▼コード(二分探索ー2回目)
def main():
# a = 0;
# b = 1:
c = 2;
d = 3;
e = 4;
f = 5;
g = 6;
h = 7;
main()
下6行のみではエラーが出ないので、上2行でエラーが発生しているようです。
▼コード(二分探索ー3回目)
def main():
# a = 0;
b = 1:
c = 2;
d = 3;
e = 4;
f = 5;
g = 6;
h = 7;
main()
“b=1:”の行を実行するとエラーが発生するようになりました。どうやらこの行でエラーが発生しているようです。
▼コード(二分探索ーチェック)
def main():
a = 0;
# b = 1:
c = 2;
d = 3;
e = 4;
f = 5;
g = 6;
h = 7;
main()
事実、“b=1:”の行のみをコメントアウトするとエラーは出ません。よく見ると、この行だけ行末尾のセミコロンがコロンになっています。
最小限のコードでデバッグする
複雑に絡み合ったコードの中からエラー箇所を特定するのは、広大な砂漠の中から一粒のダイヤモンドを探すような作業です。不具合の原因と関係なさそうな箇所を排除して、最小限のコードで不具合を再現できれば、見るべき範囲が狭まり、デバッグの負担をかなり減らすことができます。