例外

【Python】例外処理(exception)の基本 ~ try, except, raise ~

【Python】例外処理(exception)の基本 _ try, except, raise _

Pythonの例外処理(exception)の基本(try, except, raise)について解説します。

例外処理(exception)

例外(exception)とは、プログラムの処理の流れの中で発生してプログラムの継続ができなくなってしまうことを言います。各種言語で例外処理のための仕組みが用意されておりPythonでも例外処理の仕組みがあります。

例外が発生した場合には、何らかの通知をしたり、プログラムを適切に停止するなどプログラマが十分に考慮する必要があり、「例外(exception)が起きそうなところにはすべての例外処理を追加して、利用者に何が起きるのかを知らせることが重要」と言われています。

プログラム中で例外をキャッチして処理する部分を「例外ハンドラ」と言います。関数で例外をキャッチしない場合、その例外はその関数を呼び出している上位の関数までスローされていき、該当する例外ハンドラがキャッチするまで伝わっていきます。

もし適切な例外ハンドラが用意されていないと、Pythonはエラーメッセージと発生場所の情報を出力してプログラムを強制終了します。よくあるのが以下のようなインデックスエラーかなと思います。(もちろん他にも例外の種類は沢山あります)

data_list = ["A", "B", "C"]
print(data_list[5])
【実行結果例】
Traceback (most recent call last):
  File "D:/~/exception_sample.py", line 2, in <module>
    print(data_list[5])
IndexError: list index out of range

Process finished with exit code 1

上記の例は、リストのインデックス範囲を超えて処理を実行したというエラーで強制終了しています。このように例外が発生した場合は、プログラムが強制終了するのではなく、プログラマが適切な処理を記載してプログラムを「継続する」又は「終了する」といったようにしっかりとコントロールする必要があります。

以降で、Pythonでの例外処理の基本的な方法について説明していきます。

例外処理の基本的な方法

例外のキャッチ(try…except)

例外が発生する可能性があるプログラムのコード部分には、tryを使ってコードを囲み、exceptで例外をキャッチします。

使い方の基本

以下の例で、try…exceptの使い方の基本を理解しましょう。

data_list = ["A", "B", "C"]
try:
    print(data_list[5])
except IndexError as ex:
    print(ex)
print("end")
【実行結果】
list index out of range
end

上記の例では、print(data_list[5])の処理をtryの中に入れています。この処理がされた際に、このコードはIndexErrorをスローします。

スローされたIndexErrorは、次のexceptでキャッチされ、ここではas exとしているためexという変数に設定されています。受け取る変数は特に決まりはありませんが、exやeなどがよく使用されます。

print(ex)とすることで、IndexErrorの文字列(“list index out of range”)を出力することができます。ここで注目すべき部分は、それ以降のprint(‘end’)も実行されていることです。

この例では特にプログラムを終了する必要がないと判断し、次のコードを継続するというプログラムにしていることになります。

例外が発生したらプログラムを終了する

例外が発生した場合には、以下の例のようにプログラムを終了するという判断もプログラムの設計上できます。

import sys

data_list = ["A", "B", "C"]
try:
    print(data_list[5])
except IndexError as ex:
    print(ex)
    sys.exit(1)
print("end")
【実行結果】
list index out of range

Process finished with exit code 1

上記のようにすることで、終了の前に必要な処理を実施してから終了するなど、except句内で適切な処理を記載して安全にプログラムを終了することができます。

複数の例外のキャッチ

Pythonドキュメント「例外のクラス階層」で組み込みの例外クラスの階層構造を確認できますが、見ていただくと分かるように多くの組み込みの例外が用意されています。どの例外が発生するかは使用するモジュールなどにより異なっており、複数の種類の例外が発生する場合があります。

複数の例外が発生するような場合でも、exceptでは複数の種類をexceptでキャッチすることができるようになっています。以下の例で見てみましょう。

data_list = ["A", "B", "C"]

while True:
    input_value = input("input index (q to quit): ")

    if input_value == "q":
        break

    try:
        index = int(input_value)
        print(data_list[index])
    except IndexError as ex:
        print(f"bad index : {ex}")
    except ValueError as ex:
        print(f"bad value : {ex}")
【実行結果】
input index (q to quit): 0
A
input index (q to quit): 1
B
input index (q to quit): 2
C
input index (q to quit): 3
bad index : list index out of range
input index (q to quit): one
bad value : invalid literal for int() with base 10: 'one'
input index (q to quit): q

上記の例では、リストのインデックスの入力をさせて、リストの値を表示させています。

リスト内のインデックスが指定されれば、問題なくprintが実行されますが、範囲外のインデックスを指定すると、IndexErrorがスローされ、except IndexErrorの部分でキャッチされて処理がされます。

また、もしint()で型変換できない値が入力された場合には、今度はValueErrorがスローされます。この時は、except IndexErrorではキャッチされず、次のexcept ValueErrorで例外がキャッチされて処理がされます。

このように、発生しうる複数の例外を列挙してそれぞれに対する処理を記載することが可能です。

独自例外の作成と例外のスロー(raise)

上記までの例では、リスト等の標準ライブラリで定義済みであった例外の処理について紹介してきました。しかし、プログラミングをしているとアプリケーション専用の例外を作成し処理したくなる場合があります。

そのような場合には独自例外を作成し、スローすることで後のプログラムで例外処理をすることが可能です。以下の例で、独自例外の作成とスローの例を見ていきます。

class MyException(Exception):
    pass


def my_original_method(value):
    if value < 0:
        raise MyException("minus value is not allowed")
    else:
        return value


def main():
    value = -10
    try:
        print(my_original_method(value))
    except MyException as ex:
        print(ex)


if __name__ == "__main__":
    main()
【実行結果】
minus value is not allowed

上記の例では、独自に作成したmy_original_methodで、入力された引数が負の数値であったら例外を出すようにして、そうでなければそのままの値を返却します。

独自例外については、以下の部分でExceptionクラスを継承して専用例外クラスを作成しています。

class MyException(Exception):
    pass

一番シンプルな方法は、Exceptionクラスを継承して中身としてはpassとする書き方です。この書き方でもExceptionクラスを継承していることからExceptionの機能を使用できます。もちろんクラス内に処理を追加して機能拡張することも可能です。

自分で作った関数内で例外をスローする場合には、以下のようにraiseを使用します。

raise MyException("minus value is not allowed")

スローの際には、エラーの内容を示す文字列を引数に渡しておくと後の例外処理で使用することができます。

このように独自例外を作成し、必要に応じてスローすることで、後で該当する関数を使った時に、try…exceptでキャッチして例外処理をすることが可能です。

    value = -10
    try:
        print(my_original_method(value))
    except MyException as ex:
        print(ex)

例で挙げたIndexErrorやValueError、独自例外を作成した時に継承したExceptionは、BaseExceptionクラスから派生したクラスになっています。

Pythonの例外は、BaseExceptionクラスから派生したクラスである必要があります。多くの場合は、上記の例のようにExceptionクラスから継承して独自例外を作るのが一般的です。

例外のクラス階層については、以下のドキュメントを参照してください。

Pythonドキュメント「例外のクラス階層」

例外処理での注意事項

except Exceptionで安易に全ての例外をキャッチするべきではない

例外処理の基本的な使い方についてみてきましたが、例外処理での注意事項としては、「except Exception で安易に全ての例外をキャッチするべきではない」ということです。

上記で見てきた「複数の例外のキャッチ」のプログラム例を「except Exception」で少し書き換えてみます。

data_list = ["A", "B", "C"]

while True:
    input_value = input("input index (q to quit): ")

    if input_value == "q":
        break

    try:
        index = int(input_value)
        print(data_list[index])
    except Exception as ex:
        print(f"exception: {ex}")
【実行結果】
input index (q to quit): 0
A
input index (q to quit): 1
B
input index (q to quit): 2
C
input index (q to quit): 3
exception: list index out of range
input index (q to quit): one
exception: invalid literal for int() with base 10: 'one'
input index (q to quit): q

このプログラムでは、例外を全て以下の部分のように「except Exception」で受け取っています。except Exceptionは、ValueErrorかどうかに限らずその他のExceptionについてもまとめてキャッチすることができます。

except Exception as ex:
    print(f"exception: {ex}")

もちろん、このプログラムはこれまでの例と同じように動作します。しかし、この方法は安易に使うべきではありません。「プログラマが想定できていない例外に対する処理ほど怖いものはない」と言われます。

自分の書いたプログラムに対してどのような例外が発生するかを洗い出して処理を記載することはプログラムを書く人の義務ともいえます。当然、非常に大変な作業なわけですが、そういった気持ちでプログラミングをすることがプログラムの品質を上げるためにはとても重要だと言えます。

想定しきれないことはもちろんありますし、ほとんどの例外で同一の処理でよいのであれば、個別に必要な例外をキャッチして処理した後に「except Exception」でキャッチをして処置を書くということはやむを得ないかと思います。私もそうすることがほとんどですし、そういったプログラムをよく見かけます。

ただし、何が発生するかわからないからとりあえず「except Exception」で処理しようというのはプログラムを作る態度としては適切とは言えません。心構えとして「except Exception で安易に全ての例外をキャッチするべきではない」ということは覚えておいていただきたいなと思います。

まとめ

Pythonの例外処理(exception)の基本(try, except, raise)について解説しました。

基本的な例外の処理方法や複数例外の処理方法、独自例外の作成とスローについて基本的な内容を紹介してきました。

また、どのような例外が発生するか分からないから「exept Exception」で安易にとりあえずキャッチするというようなことはよくないということを説明しました。

例外が発生した場合には、何らかの通知をしたり、プログラムを適切に停止するなどプログラマが十分に考慮する必要があり「例外(exception)が起きそうなところにはすべての例外処理を追加して、利用者に何が起きるのかを知らせることが重要」と言われています。

適切に例外処理を記載して、安全で高品質なプログラムを記載できるように意識するようにしてみてください。