スポンサーリンク

保守運用観点からみるPythonのエラーハンドリングの実装方法

どうも、きっしゅです!

Pythonに限らずどんなシステム開発であってもエラーハンドリングは必須だと思います。

もしエラーハンドリングを実施していないとプログラムでエラーが発生した時にシステムが止まってしまう可能性があります。

ですが、エラーハンドリングをしっかりとしているとエラーが発生してもシステムが止まることはありません。エラー発生後に適切な処理をすれば大きな障害になることを防ぐことができます。

Pythonのエラーハンドリングはいろいろな実装方法があります。

自分はPythonでプログラムを実装しますし保守運用もします。エンジニアが良かれと思って実装したハンドリングが、保守面(障害時のログ調査)の観点から見たときに常に良い状態になっているとは限りません。

そこで今回は保守観点からPythonのエラーハンドリングに関する記事を書きました。エンジニアにも、保守チームにも快適な実装のために参考にしていただけたらと思います。

保守運用観点からみるPythonのエラーハンドリングの実装方法

まずはエラークラスの説明をします。

エラークラスの説明

PythonのエラークラスはBaseExceptionを筆頭に数多くあります。

詳細は公式に記載されていますので、興味のある方は是非ご覧ください。

ここではよく実装時によく直面するエラーを下記の表にまとめてみました。該当するエラーに直面した場合になにを調べたらいいのか参考にしていただけたらと思います。

try-exceptによる基本的なエラーハンドリング方法

まずは基本的なエラーハンドリングの実装を説明します。

Pythonではtry-exceptを使ってエラーハンドリングを実装します。
サンプルは下記にあるような感じです。

def zero_divide(a=10, b=0):
    print(f'Calculate {a} / {b}')
    answer = a / b
    print(f'Answer is {answer}')
    print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')


if __name__ == '__main__':
    print(f'Start calculation!!!')
    # エラーは発生しない想定
    print(f'Calc first time. Error will not occur.')
    zero_divide(b=5)
    
    # エラーが発生する想定
    # ZeroDivisionErrorが発生する
    print(f'Calc first time. Error will occur.')
    zero_divide()
    print(f'エラーによってこの文章は出力されません。')
    print(f'Complete calculation!!!')

当然ながらこれは0で割り算をしようとしているためエラーが発生します。
実行してみると以下のようになります。

エラーが発生するとそこで処理が止まってしまいます。上記画像でいうと2回目の計算の際にエラーが発生し、その後の処理は実施されず終了してしまっています。

実際に動かすシステムでエラーによってプログラムが終了してしまうと一大事です。
ですので、Pythonではtry-exceptを使ってエラーが発生してもプログラムが終了しないようにハンドリングをします。

実装すると以下のようになります。

def zero_divide_2(a=10, b=0):
    try:
        print(f'Calculate {a} / {b}')
        answer = a / b
        print(f'Answer is {answer}')
        print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
    except BaseException:
        print(f'>>>>> Error!!! <<<<<')


if __name__ == '__main__':
    print(f'Start calculation!!!')
    # エラーは発生しない想定
    print(f'Calc first time. Error will not occur.')
    zero_divide_2(b=5)
    
    # エラーが発生する想定
    # ZeroDivisionErrorが発生する
    print(f'Calc first time. Error will occur.')
    zero_divide_2()
    print(f'エラーが発生してもこの文章は出力されます。')
    print(f'Complete calculation!!!')

実行してみると以下のようになります。

エラーが発生しても処理が途中で終了せずに最後まで行われています。

Pythonではこのようにtry-exceptを使ってエラーが発生してもプログラムを止めないようにハンドリングをします。

ハンドリングの方法は色々とあるので、今回は基本的なハンドリング方法を紹介します。

エラーハンドリングの分岐方法

エラーが発生した時、常に一律同じ処理を実行したいとは限らないと思います。

Pythonではtry-exceptのexceptブロックで発生したエラーによってハンドリング方法を分岐することができます。
発生したエラーによって処理内容を変更したい、そんな時に活用します。

実装すると以下のようになります。

def zero_divide_3(a=10, b=0):
    try:
        print(f'Calculate {a} / {b}')
        answer = a / b
        print(f'Answer is {answer}')
        print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
        
    except ZeroDivisionError:
        print(f'>>>>> ZeroDivisionError!!! <<<<<')
        print(f'0で割り算をしようとしてエラーが発生しました。')
        print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
    except BaseException:
        print(f'>>>>> Unexpected error!!! <<<<<')
        print(f'想定外のエラーが発生しました。')
        print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')


if __name__ == '__main__':
    print(f'Start calculation!!!')
    # エラーは発生しない想定
    print(f'Calc first time. Error will not occur.')
    zero_divide_3(b=5)
    
    # 0で割り算をしようとしてエラーが発生する想定
    # ZeroDivisionErrorが発生する
    print(f'Calc first time. Error will occur.')
    zero_divide_3()
    
    # stringを渡してエラーを発生させる
    # TypeErrorが発生する
    zero_divide_3(a='0')
    print(f'エラーが発生してもこの文章は出力されます。')
    print(f'Complete calculation!!!')

実行すると以下のようになります。
発生するエラーによって出力の結果が異なることがわかります。

このようにすることでエラーハンドリングの分岐が可能です。

これを活用しエラーの発生内容によって実施すべき処理、ログの出力内容を切り分けることは保守運用面に大いに貢献するので、意識的にエラーの分岐をすることをお勧めします。

エラーの分岐、エラーの発生有無に関係なく共通の処理を実行させる方法

エラーの発生有無や、発生したエラーの分岐にかかわらず最後に共通の処理を実行したい場合があると思います。
その場合はtry-exceptの全部のブロックで同じ処理を記載すれいいのですが、その方法には以下のようなデメリットがあります。

  • 同じ処理を複数回実装する必要がある
  • 実装漏れが発生する可能性がある
  • 改修をする際の修正箇所が増える可能性がある。また修正もれの原因になる可能性がある。

Pythonでのtry-exceptには最後に必ず実行されるfinallyブロックが存在します。ですので、処理の最後に必ず実行したい処理はfinallyブロックに実装すれば一箇所の実装のみで済み、保守運用面でのコスト減に繋がります。

実装すると以下のようになります。

def zero_divide_3(a=10, b=0):
    try:
        print(f'Calculate {a} / {b}')
        answer = a / b
        print(f'Answer is {answer}')

    except ZeroDivisionError:
        print(f'>>>>> ZeroDivisionError!!! <<<<<')
        print(f'0で割り算をしようとしてエラーが発生しました。')
    except BaseException:
        print(f'>>>>> Unexpected error!!! <<<<<')
        print(f'想定外のエラーが発生しました。')
    finally:
        print('->->->->-> 最後に必ず実行される処理です。<-<-<-<-<-')
        print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')


if __name__ == '__main__':
    print(f'Start calculation!!!')
    # エラーは発生しない想定
    print(f'Calc first time. Error will not occur.')
    zero_divide_3(b=5)
    
    # 0で割り算をしようとしてエラーが発生する想定
    # ZeroDivisionErrorが発生する
    print(f'Calc first time. Error will occur.')
    zero_divide_3()
    
    # stringを渡してエラーを発生させる
    # TypeErrorが発生する
    zero_divide_3(a='0')
    print(f'エラーが発生してもこの文章は出力されます。')
    print(f'Complete calculation!!!')

実行すると以下のようにfinallyブロックの処理が毎回実行されていることがわかります。

finallyを活用すればコード量が減らすことができるので、結果的に保守運用面でのコスト減を期待することができます。

複数のエラーをハンドリングする方法

少し前にエラー分岐の説明をしましたが、そこでは特定の1つのエラーが発生した場合に分岐をする方法を紹介しました。ですが、実際の実装ではエラーAとエラーBは同じハンドリングをしたい、という場面があると思います。

そのような場合はその数だけexceptブロックを実装してもいいのですが、それをするとコード量が増えますし、保守運用面でもコストが増加します。

Pythonのexceptブロックでは複数のエラーを同じブロックでハンドリングすることが可能です。
実装すると以下のようになります。

def zero_divide_3(a=10, b=0):
    try:
        print(f'Calculate {a} / {b}')
        answer = a / b
        print(f'Answer is {answer}')

    except (ZeroDivisionError, TypeError):
        print(f'>>>>> ZeroDivisionError or TypeError!!! <<<<<')
        print(f'0で割り算もしくは文字列で割り算をしようとしてエラーが発生しました。')
    except BaseException:
        print(f'>>>>> Unexpected error!!! <<<<<')
        print(f'想定外のエラーが発生しました。')
    finally:
        print('->->->->-> 最後に必ず実行される処理です。<-<-<-<-<-')
        print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')


if __name__ == '__main__':
    print(f'Start calculation!!!')
    # エラーは発生しない想定
    print(f'Calc first time. Error will not occur.')
    zero_divide_3(b=5)
    
    # 0で割り算をしようとしてエラーが発生する想定
    # ZeroDivisionErrorが発生する
    print(f'Calc first time. Error will occur.')
    zero_divide_3()
    
    # stringを渡してエラーを発生させる
    # TypeErrorが発生する
    zero_divide_3(a='0')
    print(f'エラーが発生してもこの文章は出力されます。')
    print(f'Complete calculation!!!')

()で括ってカンマ区切りで複数指定すれば問題ありません。
実行すると以下のようになります。

カンマで区切れば何個でもハンドリングすることが可能です。
ですが、3個以上ハンドリングするような実装はあまり見かけません。

何個もエラーが発生することが想定されるようであれば、そもそものプログラムの設計を見直した方がいいと思います。

意図的にエラーを発生させる方法

プログラムを実装しているとPython的にはエラーではないが、システム的にはエラーと判断したい場合があると思います。

そのような場合は意図的にエラーを発生させて、exceptブロックでハンドリングさせることができます。

実装すると以下のようになります。
今回は計算結果が奇数の場合はエラーとみなすような実装にしてみました。

def zero_divide_3(a=10, b=0):
    try:
        print(f'Calculate {a} / {b}')
        answer = a / b
        if answer % 2 != 0:
            raise ValueError()

        print(f'Answer is {answer}')

    except (ZeroDivisionError, TypeError):
        print(f'>>>>> ZeroDivisionError or TypeError!!! <<<<<')
        print(f'0で割り算もしくは文字列で割り算をしようとしてエラーが発生しました。')
    except ValueError:
        print(f'>>>>> ValueError!!! <<<<<')
        print(f'計算結果が奇数です。結果は{answer}でした。')
    except BaseException:
        print(f'>>>>> Unexpected error!!! <<<<<')
        print(f'想定外のエラーが発生しました。')
    finally:
        print('->->->->-> 最後に必ず実行される処理です。<-<-<-<-<-')
        print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')


if __name__ == '__main__':
    print(f'Start calculation!!!')
    # エラーは発生しない想定(計算結果が偶数)
    print(f'Calc first time. Error will not occur.')
    zero_divide_3(b=5)
    
    # エラーが発生する想定(計算結果が奇数)
    # ValueErrorが発生する
    zero_divide_3(b=2)
    
    # 0で割り算をしようとしてエラーが発生する想定
    # ZeroDivisionErrorが発生する
    print(f'Calc first time. Error will occur.')
    zero_divide_3()
    
    # stringを渡してエラーを発生させる
    # TypeErrorが発生する
    zero_divide_3(a='0')
    
    print(f'エラーが発生してもこの文章は出力されます。')
    print(f'Complete calculation!!!')

実行すると以下のようになります。

計算結果が奇数の時にValueErrorが発生していると思います。

Returnをして処理を止めることも可能ですが、その分実装が複雑になりがちです。
なんでもかんでもエラーとしてraiseするのはよくありませんが、システムの仕様でエラーと定義されているものをエラーとしてハンドリングするのは問題ないと思います。

自分が実装するプログラムの要件、仕様をしっかりと理解した上で活用するとよりシンプルでわかりやすいコードが完成すると思います。

独自のエラーを定義して使う方法

Pythonでは非常に多くのエラークラスがありますが、独自のエラークラスを作成して使うことが可能です。

完全にそのプログラム固有のエラーを定義したい場合などに活用することができます。
エラー名から何が発生したのかを即座に判断できるという保守運用面でのメリットもあります。

実装すると以下のようになります。
今回は計算結果が奇数の時に使うOddNumberErrorというエラークラスを作成しました。

def zero_divide_3(a=10, b=0):
    try:
        print(f'Calculate {a} / {b}')
        answer = a / b
        if answer % 2 != 0:
            raise OddNumberError()

        print(f'Answer is {answer}')

    except OddNumberError:
        print(f'>>>>> SampleError!!! <<<<<')
        print(f'計算結果が奇数です。結果は{answer}でした。')
    except BaseException:
        print(f'>>>>> Unexpected error!!! <<<<<')
        print(f'想定外のエラーが発生しました。')
    finally:
        print('->->->->-> 最後に必ず実行される処理です。<-<-<-<-<-')
        print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')


class OddNumberError(Exception):
    pass


if __name__ == '__main__':
    print(f'Start calculation!!!')
    # エラーは発生しない想定(計算結果が偶数)
    print(f'Calc first time. Error will not occur.')
    zero_divide_3(b=5)

    # エラーが発生する想定(計算結果が奇数)
    # OddNumberErrorが発生する
    zero_divide_3(b=2)

    print(f'エラーが発生してもこの文章は出力されます。')
    print(f'Complete calculation!!!')

実行すると以下のようになります。

PEP8でエラークラスのクラス名は最後にErrorを入れるというルールがあるので、それに従うようにクラス名を命名するようにしてください。

また忘れがちなのはExceptionを継承することです。上記実装で言うとOddNumberErrorの後ろで()で囲われた部分です。

Pythonでクラスを継承する際はこのようにクラス名の後ろの括弧の中に継承したいクラスを書いてあげるだけで継承できます。複数継承する場合はカンマ区切りでクラス名を複数書けば継承できます。

エラークラスの中で変数を持たせて、独自エラークラスをもっと拡張することも可能ですが、本記事での紹介ではなく、別の記事を作成しようと思います。

まとめ

いかがでしたでしょうか?

今回はPythonでのエラーハンドリングの基本的な実装方法を紹介しました。

プログラムを実装していく上でエラーハンドリングは欠かすことができない要素だと思います。

エラーハンドリングをしていないと想定外のエラーが発生した場合に処理が止まってしまいます。場合によってはそれが原因でシステム全体が停止してしまう可能性があります。

また適切なエラーハンドリングができていると障害が発生した際のログの調査等がしやすくなるという保守運用面でのメリットや、可読性の高いプログラムだとシステム改修の際の修正コストも大幅に減ります。

ですので、最低限のエラーハンドリングの実装方法はマスターしておきたいところだと思います。

みなさまの快適なPythonライフの役に立ちますと幸いです。

Python
スポンサーリンク
ibukishをフォローする
スポンサーリンク
ibukish Lab+

コメント

タイトルとURLをコピーしました