Pythonのエラーハンドリング:try...exceptのベストプラクティスを考える
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
の項に関して重要だと思うものを書きました。
- 例外を発生させる場合、まずは組み込み例外が使えないか検討する
- 基本的に
Exception
でexcept
しない try/except
で囲むコードは最小限にする
実務で大事だと感じるのは 2 と 3 です。特に 3 は意識したほうがいいです。そして 3 を守れなくなると、複数の例外が try の処理中に入りうるのでどうしても 2 が守れなくなりがちです。
上記以外によく言われるのは、基本的に例外は想定外の入力があった場合、素直に発生するようにします。要はエラーを握りつぶさないようにするということです。
その他細かいこと
try...except
関連の書き方。
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)
結果は以下のようになる。
例外なし
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 の例外クラスについて書きました。とりあえず知ってることを取り止めもなくまとめました。 例外クラスはアプリケーションを作る上で扱い方をしっかり知っておく必要があるので、参考にしてもらえると嬉しいです。