1-4. テストとデバッグ

テストとデバッグについて説明します。

参考

仕様・テスト・デバッグ

プログラムを書くときに、実現しようとしている事柄を仕様と呼びます。

対象のプログラムが仕様に適合しているかを、実際にプログラムを動作させて検査することを、テストと呼びます。 テストの際に、テスト対象に与える入出力ペアのことを、テストケースと呼びます。

書いたプログラムが仕様に適合しているかは、一般に自明ではありません。 テストによって、仕様に反したプログラムの振舞いが、しばしば浮き彫りになります。 仕様に反したプログラムの振舞いの原因を、バグと呼び、それを取り除くことをデバッグと呼びます。

プログラミングでは、典型的には

  • 仕様を分析する

  • プログラムを書く

  • テストする

  • デバッグする

という4つの行いを、必要に応じて繰り返すことになります。

assert文

テストとデバッグに有用なのが、assert文です。 これは、assert の次に書かれた条件式が真であるべきだと仕様を宣言する文です。 偽であった場合は、AssertionError が発生してプログラムがそこで停止します。

与えられた引数を二乗する関数 square を用いた具体例を示します。

[1]:
def square(x):
    return x*x

x = -2
assert square(x) >= 0

このassert文では、仕様として条件式 square(x) >= 0 を宣言しています。 square 関数が「二乗する」という仕様に沿っているなら、その条件式は真であるべきです。 そして、実際 square はその仕様に適合しているので、ここではassert文が実行されても何も起きません。

しかし、square にバグがあった場合は、話が変わります。

[2]:
def square(x):
    return x+x # バグがある

x = -2
assert square(x) >= 0
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
/tmp/ipykernel_1581/598319922.py in <module>
      3
      4 x = -2
----> 5 assert square(x) >= 0

AssertionError:

上のセルを実行すると、 AssertionError が生じます。

このように、assert文は、それが存在する場所で、満たされていなければならない前提条件を記述するために用います。 assert文で停止したら、記述された前提条件に関わる部分にバグがあることが判明します。

テストケースは、テスト対象が満たすべき仕様という側面があるので、assert文はテストにも用いられます。

[3]:
def square(x):
    return x*x

assert square(2) == 4
assert square(-2) == 4
assert square(0) == 0

上の例では、squareに対する3つのテストケースについて、assert文でテストしています。テストケースが満たされた(つまりassert文で停止しなかった)からと言って、テスト対象の square が正しいとは言えませんが、仕様への適合度が高いことから、尤もらしいとは言えます。

エラーの分類

不正なプログラムからは、様々なエラーが生じます。

エラーには大きく分けて、構文エラー・実行時エラー・論理エラーの3つがあります。 以下では、それぞれの意味と、典型例を示します。

構文エラー

構文エラー(syntax error)とは、プログラムコードが、Pythonの構文に違反しているときに生じるエラーです。

Pythonにおける構文エラーの典型例として、

  • クォートや括弧の閉じ忘れ

  • コロンのつけ忘れ

  • インデントの崩れ

  • 全角スペースの利用

  • == の代わりに = を使う

  • 変数の代わりに文字列を使う(Cf. 2-1 文字列

などが挙げられます。

[4]:
print('This is the error) # クォートの閉じ忘れ
  File "/tmp/ipykernel_1581/4075509044.py", line 1
    print('This is the error) # クォートの閉じ忘れ
          ^
SyntaxError: unterminated string literal (detected at line 1)

[5]:
def f()  # コロンの付け忘れ
   return 1
  File "/tmp/ipykernel_1581/592770939.py", line 1
    def f()  # コロンの付け忘れ
             ^
SyntaxError: expected ':'

[6]:
def f():
return 1 # インデントの崩れ
  File "/tmp/ipykernel_1581/56312742.py", line 2
    return 1 # インデントの崩れ
    ^
IndentationError: expected an indented block after function definition on line 1

[7]:
1 + 1 # 全角スペースの利用
  File "/tmp/ipykernel_1581/156327778.py", line 1
    1 + 1 # 全角スペースの利用
       ^
SyntaxError: invalid non-printable character U+3000

上の例を実行するとわかるように、構文エラーがあると SyntaxErrorIndentationError などが発生します。 それに付随するエラーメッセージが、構文エラーの具体的内容とおおよその位置を説明してくれます。

構文エラーに直面した際は、エラーメッセージをよく読んで、原因を推察しましょう。 上の例が示すように、エラーメッセージの説明は、必ずしも分かり易くないですが、原因の位置を絞りこむには有用です。

Pythonでは、構文エラーが実行時に発生しているように見えますが、実際には、実行しようとするプログラムコードの解釈に失敗することでエラーが生じています。 つまり、構文エラーは、プログラムの実行によって生じるエラーではなく、実行できなかったことで生じるエラーです。

実行時エラー

実行時エラー(runtime error)とは、プログラムを実行した際に生じるエラー全般を指します。 簡単に言えば、プログラムを異常停止させるエラーです。

実行時エラーが生じる典型的な状況として、

  • 存在しない名前の利用(変数名・関数名・メソッド名の誤植)

  • グローバル変数のつもりでローカル変数を参照(Cf. 3-3 関数

  • ゼロによる除算

  • 辞書に登録されていないキーに対する値を取得(Cf. 3-1 辞書

  • 存在しないファイルの読み込み(Cf. 4-1 ファイル入出力

  • assert文における条件の不成立

などが挙げられます。

[8]:
undefined_variable # 未定義の変数の参照
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
/tmp/ipykernel_1581/3996331676.py in <module>
----> 1 undefined_variable # 未定義の変数の参照

NameError: name 'undefined_variable' is not defined
[9]:
x = 1
def f():
    x = x # グローバル変数のつもりでローカル変数を参照
f()
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
/tmp/ipykernel_1581/2530582621.py in <module>
      2 def f():
      3     x = x # グローバル変数のつもりでローカル変数を参照
----> 4 f()

/tmp/ipykernel_1581/2530582621.py in f()
      1 x = 1
      2 def f():
----> 3     x = x # グローバル変数のつもりでローカル変数を参照
      4 f()

UnboundLocalError: local variable 'x' referenced before assignment
[10]:
1/0 # ゼロによる除算
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
/tmp/ipykernel_1581/2808883117.py in <module>
----> 1 1/0 # ゼロによる除算

ZeroDivisionError: division by zero
[11]:
{'a': 1}['b'] # 登録されていないキーに対する値を参照
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
/tmp/ipykernel_1581/3534311223.py in <module>
----> 1 {'a': 1}['b'] # 登録されていないキーに対する値を参照

KeyError: 'b'
[12]:
open('non-existent.txt', 'r') # 存在しないファイルの読み込み
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
/tmp/ipykernel_1581/3183672649.py in <module>
----> 1 open('non-existent.txt', 'r') # 存在しないファイルの読み込み

FileNotFoundError: [Errno 2] No such file or directory: 'non-existent.txt'

実行時エラーについては、送出される例外名(上の例では NameErrorUnboundLocalErrorZeroDivisionErrorKeyErrorFileNotFoundError)が自己説明的であり、それに付随するエラーメッセージも、大抵原因を分かり易く説明してくれます。

実行時エラーに直面した際は、発生した例外名とエラーメッセージをよく読んで、エラーに関連する言語機能(たとえば辞書やファイル)の仕組みを改めて確認しましょう。

論理エラー

論理エラー(logic error)とは、プログラムを実行できるが、意図したように動作しないことを意味します。 これは、プログラムから発生するエラーではなく、プログラムを書いた人のエラーです。

バグと呼ばれるものの多くは、論理エラーです。 したがって、デバッグでは、プログラムを書いた人の意図と、プログラムの振舞いを比較検証することになります。

assert文は、仕様違反という論理エラーを、 AssertionError という実行時エラーに変換していると見做すことができます。

デバッグの具体例

デバッグの具体的なシナリオを説明します。 次の関数 median(x, y, z) は、xyz の中央値(真ん中の値)を求めようとするものです。 ただし、 xyz は相異なる数であると仮定します。

[13]:
def median(x, y, z):
    if x > y:
        x = y
        y = x
    if z < x:
        return x
    if z < y:
        return z
    return y

assert median(3, 1, 2) == 2
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
/tmp/ipykernel_1581/3638934845.py in <module>
      9     return y
     10
---> 11 assert median(3, 1, 2) == 2

AssertionError:

このように、この median は間違っています。

さて、median は、ローカル変数の xyz のいずれかを返す関数です。 これらの変数の値が期待通りの値であるか、 print を入れて印字し、観察してみましょう。

[14]:
def median(x, y, z):
    print(x, y, z)
    if x > y:
        x = y
        y = x
    print(x, y, z)
    if z < x:
        return x
    if z < y:
        return z
    return y

assert median(3, 1, 2) == 2
3 1 2
1 1 2
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
/tmp/ipykernel_1581/655774478.py in <module>
     11     return y
     12
---> 13 assert median(3, 1, 2) == 2

AssertionError:

関数の入口にある最初の print では、期待通りに実引数となる 312 が、xyz に代入されています。 しかし、2番目の print では、 3 が消えて 1 が複製されています。 このことから、この2つの print の間にあるif文が疑わしいことが分かります。

問題のif文は、xy の値を入れ替える意図があるものでした。 その意図を正しく反映すると、次のようになります。

[15]:
def median(x, y, z):
    print(x, y, z)
    if x > y:
        w = x
        x = y
        y = w
    print(x, y, z)
    if z < x:
        return x
    if z < y:
        return z
    return y

assert median(3, 1, 2) == 2
3 1 2
1 3 2

期待通りに動きました。 最後に、デバッグ用に導入した print は、median の仕様には含まれないので、きちんと消しましょう。

[16]:
def median(x, y, z):
    if x > y:
        w = x
        x = y
        y = w
    if z < x:
        return x
    if z < y:
        return z
    return y

assert median(3, 1, 2) == 2

コーディングスタイル

実は、生じたバグを取る対処法よりも、そもそもバグが生じにくくする予防法の方が大切です。 Pythonにおいて特に重要視されているのが、コーディングスタイル、つまりコードの書き方です。 読みにくい(可読性の低い)コードだと、些細なミスが生じやすく、また見つけにくいからです。

PythonではPEP8非公式日本語訳)と呼ばれる公式のスタイルガイドがあります。 PEP8には様々な側面でスタイルに関する規則があり、コードの可読性を高めることが強く推奨されています。 ここまでに扱った言語の要素について、たとえば、

  • インデントは半角スペースを4つで1レベル

  • = += == などの演算子の前後に半角スペースを1つ入れる

  • *+ の複合式では + の前後に半角スペースを1つ入れる(例:2*x + y

  • 関数の開き括弧の前にスペースを入れない

  • l I O を変数名として使わない

  • 真理値の比較に ==is を使わない

などが代表的です。

PEP8に基づいたコーディングスタイルの自動検査器もあります(参照:pycodestyle)。 オンラインサービスもいくつか利用できるので(例:PEP8 online)、適宜活用してみましょう。

PEP8には陽に言及されていないものの、プログラミング一般に重要なこともあります。 たとえば、

  • 自己説明的でない“マジックナンバー”ではなく記号的に意味がわかる変数を使う

  • 不要なコードは削除する

  • 1つの関数では1つのタスクだけを処理する

などは、可読性を上げる代表的なポイントです。

勘違いはバグを引き起こします。自らが勘違いしないコードを書くことが肝要です。

[ ]: