Pythonのエラーハンドリング:try...exceptのベストプラクティスを考える

2022-01-10
2022-01-10

2022 年一発目の技術記事です。

内容は Python の try...except、例外処理に関してです。

これまで try...exceptをなんとなくで使っていましたが、どうあるべきかを考えて自分なりのまとめとしたいと思います。 例外処理がしっかりかけるとアプリケーションの挙動をコントロールするのに役立ちします。そして単体テストも書きやすくなります。(これは個人の感想)

参考文献は、日本語だと自走プログラマーが前回に引き続きとても参考になります。 英語だとGoogle Python Style Guideがめちゃめちゃいいです。

Python の try...except の使い方

そもそもtry...exceptとは通常の処理以外のケースが発生しうる状況で、その異常ケースをコントロールしたい場合に使用します。

ゼロ除算を例にしてみてみます。以下がゼロ除算を行うコードです。

def calc_devision(x:int):
    return 100/x
calc_devision(0)

このコードを実行すると以下のような例外が発生します。

---------------------------------------------------------------------------
title: Pythonのエラーハンドリング:try...exceptのベストプラクティスを考える
createdAt: '2022-01-10'
updatedAt: '2022-01-10'
tags: ['PYTHON', '2022']
draft: false
description:  'Pythonの例外処理についてまとめてみた'
thumbnail: '/img/twitter-card.png'
---
# Pythonのエラーハンドリング:try...exceptのベストプラクティスを考える
-> 3 calc_devision(0)
/var/folders/8r/tgv96pk10zs6c5dlqdz80dyr0000gn/T/ipykernel_80515/2625511820.py in calc_devision(x)
      1 def calc_devision(x:int):
----> 2     return 100/x
ZeroDivisionError: division by zero

この例外はZeroDivisionErrorであり、0 を分母に割り算をすると発生する Python の組み込み例外です。

実際のシステムでは、予想外の入力によりシステムが停止するのは困ります。そこで、想定外の入力の場合に発生する例外をあらかじめ予測し、その際の処理を定義します。それが例外処理です。 今回の例だと以下のように例外処理してみます。

def calc_devision(x:int):
    try:
        result = 100/x
        return result
    except ZeroDivisionError as e:
        print(e)

この関数を先ほどと同様に実行するとdivision by zeroのメッセージが表示され、エラーは表示されませんでした。

このようにtry句の中に例外が発生しうるコードを入れてexcept句に想定される例外と例外が発生時の処理を書きます。 これが基本の使い方です。

よく使う組み込み例外

比較的よく使う組み込み例外を紹介します。その他の例外は公式ドキュメントを参照してください。

KeyError

存在しない辞書のキーが参照された際に発生する

コード例

dic = {"a":1, "b":2}
dic["c"]

発生する例外

KeyError                                  Traceback (most recent call last)
/var/folders/8r/tgv96pk10zs6c5dlqdz80dyr0000gn/T/ipykernel_80515/3903306493.py in <module>
      1 dic = {"a":1, "b":2}
      2
----> 3 dic["c"]
KeyError: 'c'

IndexError

リストなどで存在しないインデックスにアクセスした場合に発生する。

コード例

test_list = [0, 1, 2]
test_list[3]

発生する例外

IndexError                                Traceback (most recent call last)
/var/folders/8r/tgv96pk10zs6c5dlqdz80dyr0000gn/T/ipykernel_80515/657759428.py in <module>
      1 test_list = [0, 1, 2]
----> 2 test_list[3]
IndexError: list index out of range

ValueError

関数などに意図しない値が代入された場合に発生します。

コード例

int("hoge")

発生する例外

ValueError                                Traceback (most recent call last)
/var/folders/8r/tgv96pk10zs6c5dlqdz80dyr0000gn/T/ipykernel_80515/1819705129.py in <module>
----> 1 int("hoge")
ValueError: invalid literal for int() with base 10: 'hoge'

例外を意図的に発生させる raise

Python の例外は発生しないが、システム的に異常として扱いたい場合があります。 そういった場合には例外を意図的に発生させることもできます。 例外を意図的に発生させるにはraiseを使います。

raiseは以下のようにraiseの後にExceptionクラス(Exception クラスを継承した例外クラス)を書いて使います。

value = -1
if value < 0:
    msg = "入力値は0以上である必要があります"
    raise ValueError(msg)

独自の Exception を使う

例外を発生させたいが、組み込み例外に処理にマッチする例外がない場合があります。 そういった場合は自作の例外クラスを作成します。

自作例外クラスを作るのは簡単で、Exceptionクラスを継承したクラスを作れば完了です。

class ValidationError(Exception):
    """バリデーションエラー用の自作例外
    Args:
        Exception ([type]): [description]
    """
value = 3
if value%2 != 0:
    msg = "入力が奇数です"
    raise ValidationError(msg)

上記コードを実行すると以下のような例外が発生します。

ValidationError                           Traceback (most recent call last)
/var/folders/8r/tgv96pk10zs6c5dlqdz80dyr0000gn/T/ipykernel_80515/3159419445.py in <module>
     11 if value%2 != 0:
     12     msg = "入力が奇数です"
---> 13     raise ValidationError(msg)
ValidationError: 入力が奇数です

Python の try...except のベストプラクティスとは?

Google Python Style Guideに記載されているExceptionの項に関して重要だと思うものを書きました。

  1. 例外を発生させる場合、まずは組み込み例外が使えないか検討する
  2. 基本的にExceptionexceptしない
  3. try/exceptで囲むコードは最小限にする

実務で大事だと感じるのは 2 と 3 です。特に 3 は意識したほうがいいです。そして 3 を守れなくなると、複数の例外が try の処理中に入りうるのでどうしても 2 が守れなくなりがちです。

上記以外によく言われるのは、基本的に例外は想定外の入力があった場合、素直に発生するようにします。要はエラーを握りつぶさないようにするということです。

その他細かいこと

try...except関連の書き方。

  1. else句の利用

以下のようにtry...exceptと同じ階層にelseを書くと例外が発生しなかった場合の処理を書ける。

def calc_devision(x:int):
    result = 0
    try:
        result = 100/x
    except ZeroDivisionError as e:
        print(e)
    else:
        print("例外なし")
val = 1
calc_devision(val)

結果は以下のようになる。

例外なし
  1. finaly句の利用

以下のようにtry...exceptと同じ階層にfinalyを書くと例外が発生してもしなくても実行される処理を書ける。

def calc_devision(x:int):
    result = 0
    try:
        result = 100/x
    except ZeroDivisionError as e:
        print(e)
    else:
        print("例外なし")
    finally:
        print("終了")
    return result
val = 1
calc_devision(val)

実行結果は以下のようになる。

例外なし
終了

例外を発生させた場合は以下の出力になる。

division by zero
終了

いずれにせよfinaly句の処理が実行されていることがわかる。

例外はまとめて書ける

例えば以下のような自作例外クラスを作ったとします。

class MyAppError(Exception):
    """テスト用例外
    Args:
        Exception ([type]): [description]
    """
class UserNotFoundError(MyAppError):
    """プロジェクトが見つからなかった場合のエラー
    Args:
        MyAppError ([type]): [description]
    """
class ValidationError(MyAppError):
    """入力エラー
    Args:
        MyAppError ([type]): [description]
    """
def search_user(name:str):
    name_list = []
    if name not in name_list:
        raise UserNotFoundError("ユーザが見つかりません")
def change_name(name:str):
    name_length = len(name)
    if name_length > 10:
        raise ValidationError("ユーザ名は10文字以内にしてください")

この時以下のように継承元の例外クラスをexceptすることで継承先の例外クラスもキャッチできます。

try:
    #search_user("taro")
    change_name("tarotarotaro")
except MyAppError as e:
    print(str(e))

おわりに

今回は Python の例外クラスについて書きました。とりあえず知ってることを取り止めもなくまとめました。 例外クラスはアプリケーションを作る上で扱い方をしっかり知っておく必要があるので、参考にしてもらえると嬉しいです。

参考文献