当サイトは、アフィリエイト広告を利用しています
Pythonでスクリプトやアプリケーションを作るときの
最低押さえておくべき例外処理の用語や基本的な使い方をまとめておく。
例外(Exception)は、プログラムの実行中に発生するエラーを示す特別なイベントのこと。
例外が発生すると、通常のプログラムの流れが中断され、強制終了する
例えば下記のようなプログラムの場合は例外発生しプログラムが強制終了する
def main():# ZeroDivisionErrorが発生するresult = 10 / 0if __name__ == "__main__":main()
例外処理が発生すると下記のような問題が起こる可能性がある
例外により、プログラム全体が強制終了するので
ユーザーにとっては予期しない動作やデータの損失が発生する。
例外発生しただけだと、ユーザーは何が問題だったのか理解できず、適切な対処ができない。
※エラーメッセージは開発者向けであり、ユーザーにはわからないことが多い
プログラムがエラーを適切に処理しないと、信頼性が低くなり、
予期しないエラーが発生したときに回復できなくなる。
上記のような事態が起こるとまずいのでプログラム実行中に
例外が発生した場合に例外処理コードを実行するように実装する必要がある
例外処理コードを実装している場合、例外が発生し、プログラム中断した後
例外処理コードが実行されれば実行が再開される
この例外処理コードがtry~catch処理になる
具体的には下記のような処理の流れになる。
これにより、プログラムが強制終了することなく、エラーに対処できる。
例えば先ほどの強制終了コードにtry~catchを加えると
下記のようになる
def main():try:result = 10 / 0print("計算結果:", result)except ZeroDivisionError:print("0で割ることはできません。")if __name__ == "__main__":main()
tryブロックは例外が発生する可能性があるコードを囲む。
このブロック内で例外(ZeroDivisionError)が発生した場合は
exceptブロックに処理が移る。
※例外(ZeroDivisionError)が発生しない場合はtryブロックが実行されるだけ。
exceptブロックは、特定の例外が発生したときに実行されるコードを定義する。
上記では例外(ZeroDivisionError)が発生した場合に実行されるコードを定義している。
これによりプログラムは強制終了せずに、適切なエラーメッセージを表示したり
他のエラーハンドリングを行うことができる。
catchできるのは、記載している例外だけになる。
例えば下記のように
def main():try:raise ValueError()result = 10 / 0print("計算結果:", result)except ZeroDivisionError:print("0で割ることはできません。")if __name__ == "__main__":main()
ValueErrorを強制的に発生させた場合は、ZeroDivisionErrorが発生したわけではないので
処理はexceptブロックに移らず強制終了してしまう。
※raiseについては後述する
実際にプログラムを書く際は例外のcatchが漏れることがないように
下記のように書く。
def main():try:raise ValueError()result = 10 / 0print("計算結果:", result)except ZeroDivisionError:print("0で割ることはできません。")except ValueError as e:print(f"数値として解釈できない内容が含まれています: {e}")except Exception as e:print(f"予期せぬエラーが発生しました: {e}")if __name__ == "__main__":main()
Exceptionクラスはほとんどの例外の基底クラスであるため
使うとほぼ例外をキャッチできる。
また例外はキャッチされない限り、
呼び出し元の関数やメソッドに伝播し、最終的に、例外がキャッチされない場合
プログラムは停止し、スタックトレースが表示される
スタックトレースは例外が発生した際に
その発生箇所や原因を特定するための情報が出力されたもの。
要するに例外が発生するまでの履歴(コールスタック)
スタックトレースには下記の情報が含まれている
スタックトレースは例外が発生し、catchで補足しなかった場合
に出力される
例えば
def main():result = 10 / 0print("計算結果:", result)if __name__ == "__main__":main()
の場合、0による除算をしているためZeroDivisionErrorが発生し
かつ、try~catchでエラーハンドリングしていないのでスタックトレースが出力される
Traceback (most recent call last):File "c:\develop\exception.py", line 6, in <module>main()File "c:\develop\exception.py", line 2, in mainresult = 10 / 0~~~^~~ZeroDivisionError: division by zero
が含まれているのが確認できる
適切にtry~catchを入れた場合は
def main():try:result = 10 / 0print("計算結果:", result)except ZeroDivisionError:print("0で割ることはできません。")if __name__ == "__main__":main()
下記ようにスタックトレースは出力されず、exceptブロックで
出力している内容が出る。
上記のように例外を補足せずにスタックトレースを出すのは、実際は
プログラムが強制終了しているので開発時にはいいかもしれないが
実際のアプリケーションでは使えない。
例外をキャッチしてスタックトレースを表示し、
プログラムを強制終了させずに続行させるには
tracebackモジュールを使用する。
import tracebackdef main():try:result = 10 / 0print("計算結果:", result)except ZeroDivisionError:print("0で割ることはできません。")traceback.print_exc() # スタックトレースを表示if __name__ == "__main__":main()
実行すると
0で割ることはできません。Traceback (most recent call last):File "c:\develop\exception.py", line 5, in mainresult = 10 / 0~~~^~~ZeroDivisionError: division by zero
のようにプログラムを強制終了させることなくスタックトレースを
だすことができる。
※実際はログファイル等に書き出すことが多い。
pythonの標準の例外クラスには
などがある。
例外クラスを基準にしてPythonの例外発生の仕組みを考えると
の流れになっており、端的にいうと例外クラスからインスタンスを作っているだけだと
考えるとイメージがつきやすい。
下記のようなイメージ
def process_data(data):try:if data == "bad":raise ValueError("不正なデータが提供されました")except ValueError as value_error_instance:print(f"データ処理中にエラーが発生しました: {value_error_instance}")
上記であげたpythonの標準の例外クラスで要件を満たせない場合は
独自にカスタム例外クラスを定義することもできる。
# カスタム例外クラスclass CustomError(Exception):def __init__(self, message, detail):super().__init__(message)self.detail = detail# raiseでカスタム例外を発生させるdef process_data(data):if data == "bad":raise CustomError("不正なデータが提供されました", "データの内容が無効です")# メイン処理def main():try:process_data('bad')except CustomError as e:print(f"エラーメッセージ: {e}")print(f"詳細: {e.detail}")except Exception as e:print(f"予期せぬエラーが発生しました: {e}")if __name__ == "__main__":main()
カスタム例外クラスは、詳細なエラーメッセージや追加情報をカスタマイズできる。
注意としてはカスタム例外はPythonの標準例外とは違い、自然発生はしないので
処理の中で明示的にraiseで発生させる必要がある
raiseはPythonで明示的に例外を発生させるときに使う。
def main():try:raise ValueError()except ValueError as e:print(f"数値として解釈できない内容が含まれています: {e}")except Exception as e:print(f"予期せぬエラーが発生しました: {e}")if __name__ == "__main__":main()
明示的にValueErrorを発生させている
Pythonの標準例外は特定の状況で自動的に発生してくれるので
raiseを書かなくても問題はない。
明示的に例外を発生させるためにraiseを使う機会としては
下記が考えられる
pytest等で例外が発生するケース確認時使うことができる
例えば
class mainClass():@staticmethoddef func():result = 10 / 5return resultdef main():try:result = mainClass.func()print("計算結果:", result)except ZeroDivisionError:print("0で割ることはできません。")except Exception as e:print(f"予期せぬエラーが発生しました: {e}")if __name__ == "__main__":main()
のようなテスト対象ソースがあった場合、下記のテストコードを使えば
# test_main_module.pyimport pytestfrom app.exception import mainClass, maindef test_func_zero_division(monkeypatch):# 例外を発生させるモック関数def mock_error(*args, **kwargs):raise ZeroDivisionError()# monkeypatchを使ってmainClass.funcがZeroDivisionErrorを発生させるようにするmonkeypatch.setattr(mainClass, "func", mock_error)# 例外が発生することを確認with pytest.raises(ZeroDivisionError):mainClass.func()def test_main_zero_division(monkeypatch, capsys):# 例外を発生させるモック関数def mock_error(*args, **kwargs):raise ZeroDivisionError()# monkeypatchを使ってmainClass.funcがZeroDivisionErrorを発生させるようにするmonkeypatch.setattr(mainClass, "func", mock_error)# main関数を実行し、出力をキャプチャして確認main()captured = capsys.readouterr()assert "0で割ることはできません。" in captured.out# 正常系def test_func():result = mainClass.func()assert result == 2.0# エクセプションが発生しないことをテストdef test_func_no_exceptions():try:result = mainClass.func()assert result == 2.0except Exception as e:pytest.fail(f"Unexpected exception {e}")
明示的にExceptionを
発生させることで例外発生時の挙動のテストができる
特定の状況に応じた詳細なエラーメッセージや追加情報を提供するための
カスタム例外クラスを使う時もraiseを使うことになる
Pythonの標準例外と違い、自動で発生してくれないので
raiseを使って明示的に発生させる必要がある
# カスタム例外クラスclass CustomError(Exception):def __init__(self, message, detail):super().__init__(message)self.detail = detail# raiseでカスタム例外を発生させるdef process_data(data):if data == "bad":raise CustomError("不正なデータが提供されました", "データの内容が無効です")# メイン処理def main():try:process_data('bad')except CustomError as e:print(f"エラーメッセージ: {e}")print(f"詳細: {e.detail}")except Exception as e:print(f"予期せぬエラーが発生しました: {e}")if __name__ == "__main__":main()
raiseでカスタム例外を発生させ、catchで捕まえている。
例外をcatch後にraiseで再スローすることで
エラーの発生場所と原因を明確にすることができる
import tracebackdef process_data(data):try:if data == "bad":raise ValueError("不正なデータが提供されました")except ValueError as e:print(f"データ処理中にエラーが発生しました: {e}")raise RuntimeError("処理中のエラーが発生しました") from e# メイン処理def main():try:process_data("bad")except RuntimeError as e:print(f"再スローされたエラー: {e}")traceback.print_exc() # スタックトレースを表示if __name__ == "__main__":main()
処理の動線としては
となる。
実行後、出力されるスタックトレースは下記になる
# 例外時の出力結果データ処理中にエラーが発生しました: 不正なデータが提供されました再スローされたエラー: 処理中のエラーが発生しました# スタックトレース# 最初の例外情報(ValueError)Traceback (most recent call last):File "c:\develop\exception.py", line 6, in process_dataraise ValueError("不正なデータが提供されました")ValueError: 不正なデータが提供されましたThe above exception was the direct cause of the following exception:# 次の例外情報(RuntimeError)Traceback (most recent call last):File "c:\develop\exception.py", line 14, in mainprocess_data("bad")File "c:\develop\exception.py", line 9, in process_dataraise RuntimeError("処理中のエラーが発生しました") from eRuntimeError: 処理中のエラーが発生しました
このようにraiseで再スローを使うことで
例外が発生した場所で詳細なログやメッセージを記録し、その後再スローすることで、エラーの発生場所と原因を明確できる
下記記事では実際に再スローをしている例がある。
try~exceptで例外を補足し、例外オブジェクトに情報を追加して再スローしている。
Pythonで例外をどう扱うかについてまとめてみた。
javaなどの実装経験がある人はすんなり理解できると思う。
またtry~exceptはデコレーターを使うことで効率的に
コーディングできることがある。
デコレーターについては下記記事でまとめているので良ければ参考にしてください
当記事でまとめたのは基本的な方法であり、Pythonのフレームワークを使う場合
であれば、もっと便利にエラーハンドリングすることもできる
下記にFlaskでエラーハンドリングする場合の方法をまとめている。