標準的な例外処理
Pythonでの標準的な例外処理の仕組みは、try、exceptブロックを使った構造が基本です。
try:
# 例外が発生する可能性がある処理
except [例外の種類]:
# 例外が発生したときの処理
例えば、ゼロ除算を試みるとZeroDivisionErrorという例外が発生し、それをexceptブロックで捕捉して適切なエラーメッセージを表示してみます。
try:
result = 10 / 0 # ゼロ除算エラーが発生
except ZeroDivisionError:
print("エラー: ゼロで割ることはできません")
# 出力
エラー: ゼロで割ることはできません
・tryブロック
目的: エラーが発生する可能性のある処理を記述します。
動作: tryブロック内のコードが実行され、エラーが発生した場合は処理が中断され、対応するexceptブロックに制御が移ります。
・exceptブロック
目的: tryブロック内で発生した特定の例外を捕捉し、それに応じた処理を行います。
指定可能な例外: Pythonの組み込み例外(ValueError, ZeroDivisionError, FileNotFoundErrorなど)を指定できます。例外の種類を指定しない場合、発生したすべての例外を捕捉します。
ただ、すべての例外を捕捉すると、予期せぬエラーも処理されるため、デバッグやエラー分析が困難になります。特定の例外を指定することが推奨されます。
エラー情報を利用する
例外が発生したときにその詳細情報を取得して活用することができます。
そのためには、exceptブロックで例外オブジェクトを変数として受け取るためのasキーワードを使用します。
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"エラーが発生しました: {e}")
#出力
エラーが発生しました: division by zero
例外オブジェクトの代表的な属性
・args
: 例外の引数(メッセージなど)をタプルとして格納します。
・__str__
: 例外オブジェクトを文字列として表現します。
・__class__
: 例外の種類を示すクラス情報を取得します。
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"例外の型: {e.__class__.__name__}")
print(f"エラー内容: {e.args}")
#出力
例外の型: ZeroDivisionError
エラー内容: ('division by zero',)
・e.__class__.__name__
: 例外のクラス名(型)を取得。
・e.args
: メッセージやエラー情報をタプルとして取得。
例外発生後の後処理
finally
ブロックは、例外が発生したかどうかに関係なく必ず実行されるコードを記述するための部分です。
目的:
・必ず実行したい後片付け処理を記述する。
・たとえば、ファイルやデータベース接続を安全に閉じる。
・プログラムが例外で中断された場合でもリソースを解放する。
次の例では、ファイルを開いて処理を行った後に、エラーが発生しても必ずリソースを解放します。
try:
file = open("test.txt", "r") # ファイルを開く
content = file.read()
except FileNotFoundError:
print("エラー: ファイルが見つかりません")
finally:
print("ファイルを閉じます")
file.close()
# 出力結果(ファイルが存在しない場合)
エラー: ファイルが見つかりません
ファイルを閉じます
・ポイント
finallyブロックはエラーの有無に関係なく実行されるため、安全性が高まります。
ファイルやソケットなどのリソースを使用するコードではfinallyを使うことが推奨されます。
elseとの併用
else
とfinally
を組み合わせることで、正常時の処理と必須の後処理を両立できます。
try:
result = 10 / 2 # 正常に実行
except ZeroDivisionError:
print("エラー: ゼロ除算です")
else:
print(f"結果は {result} です")
finally:
print("終了処理を行います")
#出力結果
結果は 5.0 です
終了処理を行います
・ポイント
elseブロックで例外発生時以外の動作を定義。
finallyブロックでリソースの解放などの共通処理を実行。
例外の再スロー
例外の再スローは、例外をキャッチした後で、同じ例外を再度発生させる仕組みです。
この機能を使用することで、例外を一時的に処理しつつ、その例外の情報を保持したまま上位の呼び出し元に伝播させることができます。
再スローの目的
・部分的な処理の実行
例外を一時的にキャッチしてログやエラーメッセージを記録した後、呼び出し元に処理を委譲したい場合。
・責任の分離
特定の例外を上位層(例: 呼び出し元関数)で集中処理したい場合。
・例外情報の保持
発生した例外の詳細をそのまま伝播させ、デバッグやトラブルシューティングに役立てたい場合。
例えば、ゼロ除算エラーをキャッチしてログに記録した後、同じ例外を再スローします。
def divide(a, b):
try:
return a / b
except ZeroDivisionError as e:
print(f"ログ: エラーが発生しました: {e}")
raise # 再スロー
呼び出し元で例外を受け取ります。
try:
divide(10, 0)
except ZeroDivisionError as e:
print(f"呼び出し元で処理: {e}")
#出力
ログ: エラーが発生しました: division by zero
呼び出し元で処理: division by zero
新しい例外を発生させる
raise from
構文を使うことで、元の例外をラップしつつ新しい例外を発生させることができます。この方法では、元の例外と新しい例外の両方の情報を保持できます。
例: データベース操作のエラーハンドリング
データベース操作の例外を一時的にキャッチしてログを記録し、呼び出し元に例外を再スローするケースです。
def fetch_data(query):
try:
# 仮のデータベース操作
raise ValueError("無効なクエリ")
except ValueError as e:
print(f"ログ: データベースエラー: {e}")
raise # 再スロー
try:
fetch_data("SELECT * FROM invalid_table")
except ValueError as e:
print(f"呼び出し元で処理: {e}")
# 出力結果
ログ: データベースエラー: 無効なクエリ
呼び出し元で処理: 無効なクエリ
再スローを使用する際の注意点
過剰な再スローは避ける
・再スローを多用すると、例外が意図せず上位に伝播し、予期しない動作を引き起こす可能性があります。
例外の情報を適切に引き継ぐ
・raise fromを使用することで、元の例外と新しい例外の関連性を明確に保つことができます。
適切なログ記録
・再スロー前にエラーを記録することで、デバッグや運用時にエラーの原因を特定しやすくなります。
非同期処理におけるエラーハンドリング
非同期処理におけるエラーハンドリングは、特に並行処理を伴うアプリケーションで重要になります。
メリット
・非同期タスクごとにエラーをキャッチすることで、他のタスクへの影響を最小限に抑えることができます。
・一部のタスクが失敗しても、他のタスクは正常に実行を続けることが可能です
基本構造
非同期処理では、asyncキーワードを使用した関数が使用されます。
これらは通常、awaitで呼び出されるか、asyncio.run()で実行されます。
import asyncio
async def async_task():
try:
result = 10 / 0 # エラー発生
except ZeroDivisionError as e:
print(f"非同期タスクでエラー: {e}")
asyncio.run(async_task())
非同期関数内でのエラーハンドリングは、通常のtry-except構文とほぼ同じです。
# 出力
非同期タスクでエラー: division by zero
例: asyncio.gather()を使った複数タスク
asyncio.gather()を使用して複数のタスクを同時に実行する場合、一部のタスクで例外が発生しても、他のタスクは実行を継続します。
import asyncio
async def task1():
await asyncio.sleep(1)
print("タスク1完了")
async def task2():
await asyncio.sleep(2)
raise ValueError("タスク2でエラー発生")
async def main():
try:
await asyncio.gather(task1(), task2())
except ValueError as e:
print(f"メインでキャッチ: {e}")
asyncio.run(main())
# 出力
タスク1完了
メインでキャッチ: タスク2でエラー発生
ポイント:
asyncio.gather()
はデフォルトで全てのタスクの例外を集約します。
例外が発生すると、全体の処理が中断される可能性があるため、個別のタスクで例外をキャッチする方法が必要です。
エラーハンドリングにおけるよくある失敗例
・すべての例外を一括してキャッチする
except:
やexcept Exception:
を使ってすべての例外をキャッチするケースです。これにより、意図しない例外(例: キーボード割り込みやメモリエラー)までキャッチしてしまい、プログラムの動作が不明瞭になります。
try:
result = 10 / 0
except:
print("エラーが発生しました")
問題点
・特定の例外だけを処理したい場合でも、すべての例外をキャッチしてしまう。
・重要なシステム例外(KeyboardInterrupt, MemoryErrorなど)もキャッチされ、プログラムの終了が妨げられる。
・デバッグが困難になり、エラーの原因が隠れる。
解決策
・特定の例外のみをキャッチする。
・必要なら複数の例外をタプルで指定。
try:
result = 10 / 0
except ZeroDivisionError:
print("ゼロ除算エラーが発生しました")
except ValueError:
print("値エラーが発生しました")
・過剰な例外処理
特定の例外だけでなく、不要な場所で過剰にエラーハンドリングを行うケース。
try:
result = int("10")
print(result)
except ValueError:
print("値エラーが発生しました")
問題点
エラーが発生する可能性が低い場所でも例外処理を実装することで、コードが冗長になる。
プログラムの読みやすさと保守性が低下。
解決策
本当にエラーが発生する可能性がある箇所にのみ例外処理を記述。
try:
result = int("abc") # 本当にエラーが発生する可能性のある箇所
print(result)
except ValueError:
print("値エラーが発生しました")
・エラー処理を一括せずに分散させる
複数の箇所で同じ例外処理を繰り返す。
try:
result1 = int("abc")
except ValueError:
print("値エラーが発生しました")
try:
result2 = int("123.456")
except ValueError:
print("値エラーが発生しました")
問題点
コードの重複が発生し、保守性が低下。エラー処理の一元化ができない。
解決策
エラー処理を関数や共通のハンドラにまとめる。
def handle_error(value):
try:
return int(value)
except ValueError:
print(f"値エラーが発生しました: {value}")
result1 = handle_error("abc")
result2 = handle_error("123.456")