デバッグの方法

デバッグの流れ

デバッグは以下の図のような流れになります。一般的に、「エラーを読む」、「プリントデバッグをする」などの方法を複数組み合わせて、エラーの原因を特定していくことになります。

Notion Image

エラーを読む

エラー文にはデバッグへの手がかりとなる情報が記載されており、エラーを読むことはデバッグへの第一歩です。ただプログラミングを始めたばかりの人には、エラーを読み解くのは難しく、最終的に読まなくなってしまうこともあります。エラーを読み解くのが難しい理由には次のようなものがあります。

  • 長くて読みづらい
  • 読んでもエラーの原因がわからない
  • 英語で書かれている
  • しかし、エラーは常に決まった型にしたがって表示されるため、エラーの構造と頻出する英語表現を一度理解してしまえば、エラーを読み解くのはずっと簡単になります。例えば、次のようなPythonのエラーをみてみましょう。

    ▼ コード

    name = "山田"
    print(nama)

    ▼ エラー

    Notion Image

    Pythonに限らずどんな言語でも、エラーは「エラーの種類」、「エラーメッセージ」、「トレースバック(スタックトレース)」の3つの要素から構成されます。「エラーの種類」と「エラーメッセージ」に頻出する英語表現は決まっていて、基本的なものを以下の表に示します。

    Pythonの基本的なエラー一覧

    エラーの分類エラーの種類エラーメッセージエラーの意味
    構文に関するエラーSyntaxErrorinvalid syntax構文が正しくない
    同上同上keyword argument repeated関数呼び出しの中で引数が重複して与えられている
    同上同上unterminated string literal文字列で'や"を締め忘れている
    同上同上Missing parentheses in call to ‘x’xを呼び出すのに必要な括弧が不足している
    同上IndentationErrorunexpected indent不要なインデントがある
    同上同上unindent does not match any outer indentation levelインデントの数が一致しない
    同上同上expected an indented blockインデントが必要な場所にインデントがない
    インポートに関するエラーImportErrorcannot import name 'x' from 'y'モジュールにオブジェクトが含まれていないなどの理由により、 モジュールyからオブジェクトxをインポートできない
    同上ModuleNotFoundErrorNo 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異なる型を連結しようとしている
    同上ValueErrorcould not convert string to float: 'x'関数に想定していない値が渡された
    同上ZeroDivisionErrordivision by zero0で割り算が行われた
    同上NameErrorname 'x' is not defined変数や関数が定義されていないか参照する名前のスペルを間違えている
    インデックスやキーに関するエラーIndexErrorlist index out of rangeリストやタプルなどの範囲外のインデックスが指定されている
    同上KeyError同上辞書の存在しないキーが指定されている
    ファイルやディレクトリに関するエラーFileNotFoundErrorNo such file or directory指定したファイルやディレクトリが存在しない
    同上FileExistsErrorFile exists作成しようとしたファイルがすでに存在する

    「トレースバック(スタックトレース)」はエラーが発生するまでの流れを辿ったものであり、処理がどのように進んだかを表す履歴情報です。Pythonの場合、一番下にエラーが発生した最終地点が記載され、その上を見るとどのような順序で処理が実行されたか辿ることができます。

    ▼ コード

    def fn1():
      fn2()
    
    def fn2():
      fn3()
    
    def fn3():
      x
    
    fn1()

    ▼エラー

    Notion Image

    エラーが発生した際には、まずどこでエラーが発生したか(最終地点)を確認し、その時点でエラーを解決できるのであればそれより前の履歴情報を読む必要はありません。そのため、上から順にすべての行を読むのではなく、まずは最終地点の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

    このコードを実行すると以下の出力が得られます。

    ▼出力

    Notion Image

    この出力を見ると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

    ▼出力(修正後)

    Notion Image

    このようにプリントデバッグでは、変数の中身、関数内部の処理、if文やfor文内の処理経路を確認しながら不具合の原因を探っていきます。漠然とコードを眺めているより、プリント文で地道に情報を集めていくほうが、効率的な場合が多いです。

    デバッガを使う

    デバッガはより効率的にプリントデバッグを行うためのツールで、特定の場所で処理を中断して、その時点での変数の中身を確認したり、新しいコードを実行したりできます。

    VSコードでのデバッガの使い方

    左端のツールバーの「実行とデバッグ」アイコンから「デバッグの開始」をクリックするとデバッガを利用できます。初回はlaunch.jsonを作成する必要がありますが、今回はデフォルト設定のままでかまいません。

    Notion Image

    ブレークポイント

    ブレークポイントを設定すると、その位置でプログラムの処理を一時停止して、変数の中身などを確認できます。エディター上の該当の行の左端の余白をクリックすると、ブレークポイントを設定できます。

    Notion Image

    実際、ブレークポイントを設定した状態で「デバッグの開始」を実行すると、ブレークポイントの位置で処理が一時停止します。

    Notion Image

    また、左側の「v 変数」に、ブレークポイントの位置での変数の値が表示されます。

    Notion Image

    デバッグコンソールでコードを実行すると、ブレークポイントの位置での変数の値を使って、新しいコードを試すことができます。

    Notion Image

    上記の操作が確認できたら、画面右上に表示される「続行」ボタンを押して処理を進めるか、「停止」ボタンを押してデバッグを終了します。

    Notion Image

    最後に、ブレークポイント(赤丸)をクリックすればブレークポイントを解除することができます。

    条件つきブレークポイント

    for文など何度もブレークポイントの処理が実行される場合、毎回ブレークポイントの位置で処理が停止されてしまいます。ある条件を満たしているときだけ処理を停止したい場合は、ブレークポイントに条件をつけることができます。

    ブレークポイント(赤丸)を右クリックし、「ブレークポイントの編集」をクリックします。

    Notion Image

    条件式に”i==3”と入力します。

    Notion Image

    Enterを押すと条件つきブレークポイントの設定が完了します。

    Notion Image

    実際、「デバッグの開始」を実行すると、条件式”i==3”をみたしたときに処理を停止できていることが確認できます。

    Notion Image

    上記の操作が確認できたら、画面右上に表示される「続行」ボタンを押して処理を進めるか、「停止」ボタンを押してデバッグを終了します。

    Notion Image

    ステップ実行

    ステップ実行は、ブレークポイントで停止した位置から1ステップずつ処理を実行するための機能で、処理の詳細を理解したいときに役立ちます。ステップ実行には、ステップイン、ステップオーバー、ステップアウトの3つがあります。

    機能説明
    ステップイン (Step In)現在の行を実行して1行進める。 関数呼び出しがあれば関数の中に入り、 すべてのコードを1行ずつ実行していく。
    ステップオーバー (Step Over)現在の行を実行して1行進める。 関数呼び出しがあっても関数の中には入らず、 関数を実行して次の行に進む。
    ステップアウト (Step Out)現在の関数の終わりまでコードを実行し、 呼び出し元に戻る。

    各機能に対応するボタンは、「デバッグの開始」を実行すると画面の右上に表示されます。

    Notion Image

    ネットで調べる

    外部ライブラリを利用している時にエラーが出る場合、まずは公式ドキュメントを見て、ライブラリの使い方に誤りがないか確認しましょう。

    公式ドキュメントの情報だけではエラーを解決できない場合、エラーメッセージをそのまま検索し、誰かが同じエラーを解決してくれていないか確認しましょう。この時、自分の環境固有の情報(バージョン、ファイル名、ソースコードの行番号など)が含まれないように注意してください。

    日本語で調べてもヒットしない場合は、英語でも検索してみましょう。検索結果のURLの末尾に「&lr=-lang_ja」と入力すれば、日本語以外のサイトを対象に検索できます。

    AIを活用する

    AIを使ったサポートツールも続々と登場してきています。AIに直接エラー文を投げ込むだけでエラーの原因がわかることもあります。ただし、入力した文章はすべて収集されていると考えたほうが良く、シークレットキーや社内の非公開コードなど機密情報の取り扱いには十分に注意してください。

    質問する

    調べてもエラーの原因が特定できない場合、「Stack Overflow」のようなプログラミングに関するコミュニティに質問することも選択肢の一つになります。質問する場合は回答しやすいように以下の点に注意するようにしましょう。

  • 具体的なタイトルをつける コミュニティなどで回答してくれるユーザーは、質問のタイトルを見て回答するかどうか決めることが多いです。「なぜか動きません・・・助けてください」というタイトルではなく、「ReactのuseStateを使っているが変更が反映されない」のような具体的なタイトルの方が回答してもらいやすくなります。
  • 問題の詳細を網羅的に記述する 問題の詳細が十分に記述されていなければ、エラーの原因を特定できないため回答が得られない場合が多いです。エラー文が出る場合はエラー文すべてを記述する、該当のコードがあれば前後のコードも含めて適切な部分だけ記述する、利用している言語やライブラリ、動作環境などの名称とバージョンを記述する、操作や進行した作業手順を簡潔に箇条書きで記述する、期待する動作や目標を記述するなど、問題の詳細を網羅的に記述することが望ましいです。
  • 自分で解決することで身につく力もありますが、15分以上膠着状態が続くような場合は、自分一人で抱え込まずに質問してしまったほうが良いかもしれません。

    デバッグのためのTips

    少しずつコードを追加する

    一度に大量の変更を加えると、どこでバグが入り込んだのかわからなくなる。少しずつコードを変更していくことで、不具合原因の箇所が限定されデバッグが簡単になる。

    Notion Image

    経路をたどる

    プリントデバッグでは、変数の中身を確認するだけでなく、プログラムがどのような経路をたどって実行されているか確認するのに役立ちます。原因箇所を特定するためには、該当の処理がそもそも実行されているか、処理が最後まで実行されているかをちゃんと把握することで、不具合の原因を突き止められることがあります。

    ▼コード(プリントデバッグ)

    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:”の行のみをコメントアウトするとエラーは出ません。よく見ると、この行だけ行末尾のセミコロンがコロンになっています。

    最小限のコードでデバッグする

    複雑に絡み合ったコードの中からエラー箇所を特定するのは、広大な砂漠の中から一粒のダイヤモンドを探すような作業です。不具合の原因と関係なさそうな箇所を排除して、最小限のコードで不具合を再現できれば、見るべき範囲が狭まり、デバッグの負担をかなり減らすことができます。

    Notion Image

    参考資料


    著者画像

    ゆうき

    2018/04からITエンジニアとして活動、2021/11から独立。主な使用言語はPython, TypeScript, SAS, etc.