当サイトは、アフィリエイト広告を利用しています

【Python】Pythonの例外処理(Exception)の基本をまとめた

作成日:2024月07月24日
更新日:2024年09月04日

Pythonでスクリプトやアプリケーションを作るときの
最低押さえておくべき例外処理の用語や基本的な使い方をまとめておく。

そもそも例外(Exception)って何?

例外(Exception)は、プログラムの実行中に発生するエラーを示す特別なイベントのこと。
例外が発生すると、通常のプログラムの流れが中断され、強制終了する
例えば下記のようなプログラムの場合は例外発生しプログラムが強制終了する

main.py
def main():
# ZeroDivisionErrorが発生する
result = 10 / 0
if __name__ == "__main__":
main()

例外処理が発生すると下記のような問題が起こる可能性がある

プログラムのクラッシュ

例外により、プログラム全体が強制終了するので
ユーザーにとっては予期しない動作やデータの損失が発生する。

ユーザーへのフィードバックがない

例外発生しただけだと、ユーザーは何が問題だったのか理解できず、適切な対処ができない。
※エラーメッセージは開発者向けであり、ユーザーにはわからないことが多い

ロバスト性の欠如

プログラムがエラーを適切に処理しないと、信頼性が低くなり、
予期しないエラーが発生したときに回復できなくなる。

try~catchとは?

上記のような事態が起こるとまずいのでプログラム実行中に
例外が発生した場合に例外処理コードを実行するように実装する必要がある

例外処理コードを実装している場合、例外が発生し、プログラム中断した後
例外処理コードが実行されれば実行が再開される

この例外処理コードがtry~catch処理になる
具体的には下記のような処理の流れになる。

  1. 通常のコード実行: プログラムは通常通り実行。
  2. 例外の発生: 何らかのエラーが発生すると、例外がスローされる。
  3. 例外処理コードの検索: Pythonインタープリタは、発生した例外を処理できる except ブロックを探す。
  4. 例外処理コードの実行: 適切な except ブロックが見つかると、そのブロック内のコードが実行される。
  5. 通常の流れに戻る: 例外処理が完了すると、プログラムの実行は通常の流れに戻ります。

これにより、プログラムが強制終了することなく、エラーに対処できる。

例えば先ほどの強制終了コードにtry~catchを加えると
下記のようになる

try~catch
def main():
try:
result = 10 / 0
print("計算結果:", result)
except ZeroDivisionError:
print("0で割ることはできません。")
if __name__ == "__main__":
main()
  • ZeroDivisionError発生し、プログラムが中断される
  • 例外処理コードとしてexceptブロックが実行される

tryとは?

tryブロックは例外が発生する可能性があるコードを囲む。
このブロック内で例外(ZeroDivisionError)が発生した場合は
exceptブロックに処理が移る。
※例外(ZeroDivisionError)が発生しない場合はtryブロックが実行されるだけ。

catchとは?

exceptブロックは、特定の例外が発生したときに実行されるコードを定義する。
上記では例外(ZeroDivisionError)が発生した場合に実行されるコードを定義している。

これによりプログラムは強制終了せずに、適切なエラーメッセージを表示したり
他のエラーハンドリングを行うことができる。

catchする例外について

catchできるのは、記載している例外だけになる。
例えば下記のように

別の例外
def main():
try:
raise ValueError()
result = 10 / 0
print("計算結果:", result)
except ZeroDivisionError:
print("0で割ることはできません。")
if __name__ == "__main__":
main()

ValueErrorを強制的に発生させた場合は、ZeroDivisionErrorが発生したわけではないので
処理はexceptブロックに移らず強制終了してしまう。
※raiseについては後述する

例外処理の構造

実際にプログラムを書く際は例外のcatchが漏れることがないように
下記のように書く。

main.py
def main():
try:
raise ValueError()
result = 10 / 0
print("計算結果:", result)
except ZeroDivisionError:
print("0で割ることはできません。")
except ValueError as e:
print(f"数値として解釈できない内容が含まれています: {e}")
except Exception as e:
print(f"予期せぬエラーが発生しました: {e}")
if __name__ == "__main__":
main()
  • 起こりえる例外をexceptブロック羅列してそれぞれの例外処理を書く
  • 最終的にExceptionで例外処理をするようにする

Exceptionクラスはほとんどの例外の基底クラスであるため
使うとほぼ例外をキャッチできる。

また例外はキャッチされない限り、 呼び出し元の関数やメソッドに伝播し、最終的に、例外がキャッチされない場合
プログラムは停止し、スタックトレースが表示される

スタックトレースについて

スタックトレースは例外が発生した際に
その発生箇所や原因を特定するための情報が出力されたもの。
要するに例外が発生するまでの履歴(コールスタック)

スタックトレースには下記の情報が含まれている

  • モジュール名
  • 行番号
  • 関数名
  • エラーメッセージ

スタックトレースは例外が発生し、catchで補足しなかった場合
に出力される

例えば

スタックトレース
def main():
result = 10 / 0
print("計算結果:", 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 main
result = 10 / 0
~~~^~~
ZeroDivisionError: division by zero
  • モジュール名
  • 行番号
  • 関数名
  • エラーメッセージ

が含まれているのが確認できる

適切にtry~catchを入れた場合は

エラーハンドリング追加
def main():
try:
result = 10 / 0
print("計算結果:", result)
except ZeroDivisionError:
print("0で割ることはできません。")
if __name__ == "__main__":
main()

下記ようにスタックトレースは出力されず、exceptブロックで
出力している内容が出る。

スタックトレースの出し方

上記のように例外を補足せずにスタックトレースを出すのは、実際は
プログラムが強制終了しているので開発時にはいいかもしれないが
実際のアプリケーションでは使えない。

例外をキャッチしてスタックトレースを表示し、
プログラムを強制終了させずに続行させるには
tracebackモジュールを使用する。

tracebackモジュール
import traceback
def main():
try:
result = 10 / 0
print("計算結果:", 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 main
result = 10 / 0
~~~^~~
ZeroDivisionError: division by zero

のようにプログラムを強制終了させることなくスタックトレースを
だすことができる。 ※実際はログファイル等に書き出すことが多い。

例外クラスについて

pythonの標準の例外クラスには

  • ValueError(無効な引数が与えられた場合)
  • TypeError(型が不適切な場合)
  • IndexError(リストやタプルなどのインデックスが範囲外の場合)
  • KeyError(辞書に存在しないキーでアクセスした場合)

などがある。

例外クラスを基準にしてPythonの例外発生の仕組みを考えると

  1. 処理中に例外が発生する
  2. 発生した例外クラスの例外インスタンスを作成する
  3. プログラムのフローが中断される
  4. 例外処理のためのexceptブロックを探す
  5. exceptブロックがあれば処理を投げる(同時に例外インスタンスも渡せる)
  6. ない場合は強制終了し、スタックトレース表示される

の流れになっており、端的にいうと例外クラスからインスタンスを作っているだけだと
考えるとイメージがつきやすい。

下記のようなイメージ

process_data
def process_data(data):
try:
if data == "bad":
raise ValueError("不正なデータが提供されました")
except ValueError as value_error_instance:
print(f"データ処理中にエラーが発生しました: {value_error_instance}")
  • ValueErrorクラスから"不正なデータが提供されました"を引数にしてValueErrorインスタンスを作る
  • exceptブロックの「as」で例外インスタンスを受け取り、例外処理を行う

カスタム例外クラス

上記であげた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)について

raiseはPythonで明示的に例外を発生させるときに使う。

raise
def main():
try:
raise ValueError()
except ValueError as e:
print(f"数値として解釈できない内容が含まれています: {e}")
except Exception as e:
print(f"予期せぬエラーが発生しました: {e}")
if __name__ == "__main__":
main()

明示的にValueErrorを発生させている

raiseの使いどころ

Pythonの標準例外は特定の状況で自動的に発生してくれるので
raiseを書かなくても問題はない。

明示的に例外を発生させるためにraiseを使う機会としては
下記が考えられる

  • テストで挙動確認をする時
  • カスタム例外クラスを使用する時
  • 例外を再スローする時

テストで挙動確認をする時

pytest等で例外が発生するケース確認時使うことができる
例えば

exception.py
class mainClass():
@staticmethod
def func():
result = 10 / 5
return result
def main():
try:
result = mainClass.func()
print("計算結果:", result)
except ZeroDivisionError:
print("0で割ることはできません。")
except Exception as e:
print(f"予期せぬエラーが発生しました: {e}")
if __name__ == "__main__":
main()

のようなテスト対象ソースがあった場合、下記のテストコードを使えば

test_exception.py
# test_main_module.py
import pytest
from app.exception import mainClass, main
def 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.0
except 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 traceback
def 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()

処理の動線としては

  • main関数でtryブロックでprocess_data関数を実行
  • process_data関数のtryブロックでValueErrorを発生させる(raiseする)
  • process_data関数のcatchブロックでValueErrorを補足する
  • process_data関数のcatchブロックでRuntimeErrorを発生させる(raiseする)
  • main関数でcatchブロックでRuntimeErrorを補足する
  • スタックトレースを出力する

となる。

実行後、出力されるスタックトレースは下記になる

スタックトレース
# 例外時の出力結果
データ処理中にエラーが発生しました: 不正なデータが提供されました
再スローされたエラー: 処理中のエラーが発生しました
# スタックトレース
# 最初の例外情報(ValueError)
Traceback (most recent call last):
File "c:\develop\exception.py", line 6, in process_data
raise 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 main
process_data("bad")
File "c:\develop\exception.py", line 9, in process_data
raise RuntimeError("処理中のエラーが発生しました") from e
RuntimeError: 処理中のエラーが発生しました
  • raiseした例外がそれぞれ出力されている
  • 「The above exception was the direct cause of the following exception」で二つの例外の因果関係を示している

このようにraiseで再スローを使うことで
例外が発生した場所で詳細なログやメッセージを記録し、その後再スローすることで、エラーの発生場所と原因を明確できる

下記記事では実際に再スローをしている例がある。
try~exceptで例外を補足し、例外オブジェクトに情報を追加して再スローしている。

まとめ

Pythonで例外をどう扱うかについてまとめてみた。
javaなどの実装経験がある人はすんなり理解できると思う。

またtry~exceptはデコレーターを使うことで効率的に
コーディングできることがある。
デコレーターについては下記記事でまとめているので良ければ参考にしてください

当記事でまとめたのは基本的な方法であり、Pythonのフレームワークを使う場合
であれば、もっと便利にエラーハンドリングすることもできる

下記にFlaskでエラーハンドリングする場合の方法をまとめている。

参考

関連記事

新着記事

タグ別一覧
top