とりゅふの森

GCPデータエンジニアとして生きる

【Python入門】エラー・例外と例外処理のキホンを学ぶ

f:id:true-fly:20210727234535p:plain

プログラム書いている人で、必ずといっても過言ではないくらい躓きがちなものが、エラー、例外処理だと思います。
今回はPythonにおける例外処理の基礎についてまとめました。
本記事のコードはすべて、Python 3.7.6で実行しています。

入門 Python 3 第2版

入門 Python 3 第2版

Amazon

エラー、例外

Pythonなどのプログラム言語には、例外(Exception)とエラー(Error)言った概念があります。しばしばこれらの違いを問われますが、両者の違いを明確には定義できないらしいです。 本記事ではそれぞれ以下のように扱おうと思います。

  • エラー
    • プログラムが実行不可能な状態であること(Syntax Error)
    • プログラムが異常終了する状態であること
  • 例外
    • プログラム内で発生した対処が必要な状態のこと

SyntaxError

以下のように、関数の後ろに:がないなど、構文エラーの場合、プログラムは実行されず、異常終了。エラーを出力します。

def hello()
    return 'Hello'

print(hello())
  File "truefly.py", line 1
    def hello()
              ^
SyntaxError: invalid syntax

例外

例えば以下のようなゼロ除算をしたとき、プログラムは2行目でZeroDivisionErrorという例外を発生させます。ZeroDivisionErrorは例外クラスと呼ばれるものです。例外クラスは、ExceptionKeyErrorTypeErrorなどの、Pythonで元々使える組み込み例外と、後述する自作の例外クラスがあります。
Pythonはこの例外クラスを返すことで、どのような例外が発生したのかを判別することができます。

def divide(n, m):
    print(n / m)

divide(10, 0)
Traceback (most recent call last):
  File "truefly.py", line 4, in <module>
    divide(10, 0)
  File "truefly.py", line 2, in divide
    print(n / m)
ZeroDivisionError: division by zero

では、このような例外が発生したときの対処法について考えていきましょう。

例外処理

try exceptで例外が発生時の処理を書く

プログラムが例外を発生させたときに実行するときの処理のことを、例外処理といいます。そのままですね。例えば除算する処理でゼロ除算が発生した場合、何も対処しないと、ZeroDivisionErrorが発生して処理が異常終了しますが、
実際のプロダクトでは、
「ゼロで割ることはできません!」
といった文言を出力して、再度入力を促したいですよね。
そういった例外処理をするとき、Pythonでは、tryexceptを使います。

def divide(n, m):
    try:
        print(n / m)
    except:
        print('エラーが発生しました')

divide(10, 0)
エラーが発生しました

tryの中に例外が発生しうる処理を書き、exceptの中に、try内で例外が発生した時に実行する処理を書きます。この書き方の場合、どんな例外が発生した場合でも、except内の処理が実行されます。exceptの後ろに、検知したい例外を書くことで、特定の例外が発生した時に例外処理することもできます。

def divide(n, m):
    try:
        print(n / m)
    except ZeroDivisionError:
        print('エラーが発生しました')

divide(10, 0)
divide(10, 'zero')
エラーが発生しました
Traceback (most recent call last):
  File "truefly.py", line 8, in <module>
    divide(10, 'zero')
  File "truefly.py", line 3, in divide
    print(n / m)
TypeError: unsupported operand type(s) for /: 'int' and 'str'

最初のdivide(10, 0)ではZeroDivisionErrorが発生しますが、無事例外処理がされています。その後のdivide(10, 'zero')では、TypeErrorが発生したため、例外処理がされず、エラーとなりました。

exceptは、1つのtryに対して、複数書くこともできます。また、except 例外クラス as 変数名と書くことで、例外処理内で例外オブジェクトを利用することもできます。

def divide(n, m):
    try:
        print(n / m)
    except ZeroDivisionError as e:
        print('ゼロ除算のエラー')
        print(e)
    except TypeError as e:
        print('型のエラー')
        print(e)
    

divide(10, 2)
divide(10, 0)
divide(10, 'zero')
5.0
ゼロ除算のエラー
division by zero
型のエラー
unsupported operand type(s) for /: 'int' and 'str'

try except elseで例外が発生しなかったときの処理を書く

exceptは例外が発生したときの処理を書くことができますが、例外が発生しなかったときの処理を書きたいときは、 try except elseを使います。

def divide(n, m):
    ans = 0
    try:
        ans = n / m
        print(ans)
    except ZeroDivisionError as e:
        print('ゼロ除算のエラー')
        print(e)
    finally:
        print('処理を終了します')

divide(10, 0)
divide(10, 2)
divide(10, 'zero')
ゼロ除算のエラー
division by zero
処理を終了します
答え
5.0
処理を終了します

ZeroDivisionError発生時はexcept句内の処理を、それ以外の場合はelse句内の処理を実行してくれるようになりました。
個人的には例外が発生しなかったら、try句の処理が最後まで走るので、わざわざelseを書くことは多くはないかなと思っています。

try except finallyで例外が発生したとき、しなかったときの共通処理を書く

例外が発生したとき、例外が発生しなかったとき例外が発生したときの書き方はわかりましたが、例外が発生したとき、しなかったときの共通処理もあると便利ですよね。そんなときはtry except finallyを使います。

def divide(n, m):
    ans = 0
    try:
        ans = n / m
        print(ans)
    except ZeroDivisionError as e:
        print('ゼロ除算のエラー')
        print(e)
    finally:
        pass
        print('処理を終了します')

divide(10, 0)
divide(10, 2)
divide(10, 'zero')
ゼロ除算のエラー
division by zero
処理を終了します
5.0
処理を終了します
処理を終了します
Traceback (most recent call last):
  File "main.py", line 14, in <module>
    divide(10, 'zero')
  File "main.py", line 4, in divide
    ans = n / m
TypeError: unsupported operand type(s) for /: 'int' and 'str'

divide()の1回目の呼び出しはZeroDivisionErrorが発生しているため、exceptfinally句内の処理を実行されています。
2回目の呼び出しは例外が発生していないため、try句が最後まで処理され、finally句内の処理も実行されています。
そして3回目の処理は、TypeErrorが発生しているため、try句が最後まで処理されず、finally句内の処理のみを実行して、異常終了しました。
DBのコネクションクローズや、ロールバック処理など、正常時でも異常時でも必ず実行したい処理をfinally句内に書けるので便利です。

まとめ

以上、Pythonのエラー、例外と、例外処理について学びました。
プログラムはどんなに成熟したプログラマーがテストを重ねたとしても、通信障害だったり、実装時に意図していなかった操作などが原因で、
こういった例外が発生することは往々にしてあります。そういったケースにも対応できるよう、予め例外処理を実装しておき、可用性の高いシステムをつくりましょう。 次回は例外の発生方法と、例外クラスについてまとめようと思います。