Day54|例外設計:握りつぶさない
はじめに
例外処理は「エラーが起きたとき何をするか」を決める仕組みです。
try/exceptの基本はDay19で学びました。
今日は「例外を正しく扱う設計」に焦点を当てます。
最も避けるべき悪習「例外の握りつぶし」から始めましょう。
1. 例外の握りつぶしとは
# BAD: 何もしないexcept
def load_config(path):
try:
with open(path) as f:
return f.read()
except:
pass # エラーが起きても何もしないこれを「例外の握りつぶし」と言います。エラーが起きても気づけないため、後で全く別の場所で原因不明のバグとして現れます。
2. 何を握りつぶしてはいけないか
`except`で捕まえる範囲を「広すぎず」にすることが重要です。
# BAD: 全例外を捕まえる(except:またはexcept Exception:)
try:
result = some_function()
except: # KeyboardInterruptやSystemExitまで握りつぶす
pass
# GOOD: 想定している例外だけを捕まえる
try:
with open(path) as f:
return f.read()
except FileNotFoundError:
print(f"ファイルが見つかりません: {path}")
return None`except:`(裸のexcept)は`KeyboardInterrupt`(Ctrl+C)や`SystemExit`まで捕まえてしまうため、特別な理由がない限り使いません。
3. 例外を捕まえたら何かする
例外を捕まえた場合は、必ず何かアクションを取ります。
# BAD: 捕まえて何もしない
try:
value = int(user_input)
except ValueError:
pass
# GOOD: ログ出力・デフォルト値設定・再送出のいずれかをする
import logging
def parse_int(user_input: str, default: int = 0) -> int:
"""文字列を整数に変換する。変換できない場合はdefaultを返す。"""
try:
return int(user_input)
except ValueError:
logging.warning(f"整数への変換に失敗しました: {user_input!r}")
return default4. 例外を再送出する(raise)
捕まえた例外に情報を付け加えて再送出することもできます。
def load_user_data(filepath: str) -> dict:
"""ユーザーデータをJSONファイルから読み込む。"""
try:
with open(filepath, encoding="utf-8") as f:
import json
return json.load(f)
except FileNotFoundError as e:
raise FileNotFoundError(
f"ユーザーデータファイルが見つかりません: {filepath}"
) from e
except json.JSONDecodeError as e:
raise ValueError(
f"ユーザーデータのJSON形式が不正です: {filepath}"
) from e`raise ... from e` と書くと、元の例外を保持したまま新しい例外を送出できます。デバッグ時に原因が追いやすくなります。
5. finallyで後処理を保証する
`finally`ブロックは、例外が起きてもそうでなくても必ず実行されます。
def write_file(path: str, content: str) -> None:
"""ファイルに内容を書き込む。"""
f = None
try:
f = open(path, "w", encoding="utf-8")
f.write(content)
except OSError as e:
raise OSError(f"ファイルの書き込みに失敗しました: {path}") from e
finally:
if f:
f.close() # 例外が起きてもファイルを確実に閉じるただしファイルを扱う場合は`with`文(コンテキストマネージャ)を使う方が安全です。
def write_file(path: str, content: str) -> None:
try:
with open(path, "w", encoding="utf-8") as f:
f.write(content)
except OSError as e:
raise OSError(f"ファイルの書き込みに失敗しました: {path}") from e演習
以下のコードの問題点を指摘し、修正してください。
def get_age(data, key):
try:
return int(data[key])
except:
passまとめ
| やってはいけないこと | 正しい対処 |
|---|---|
| `except: pass`(握りつぶし) | 必ず何かアクションを取る |
| `except:`(裸のexcept) | 具体的な例外クラスを指定する |
| エラーを無視して続行 | ログに残す・デフォルト値を返す・再送出する |
例外処理の基本方針:
想定している例外だけを捕まえる
捕まえたら必ず何かする(ログ・デフォルト値・再送出)
握りつぶさない
模範例
import logging
from typing import Any
def get_age(
data: dict[str, Any],
key: str,
default: int | None = None,
) -> int | None:
"""辞書から年齢を取得し、整数へ変換する。
キーが存在しない場合や、値を整数へ変換できない場合は
警告ログを出力してdefaultを返す。
"""
try:
return int(data[key])
except KeyError:
logging.warning(
"指定されたキーが見つかりません: %r",
key,
)
return default
except (TypeError, ValueError):
logging.warning(
"年齢を整数へ変換できません: key=%r, value=%r",
key,
data.get(key),
)
return defaultいいなと思ったら応援しよう!
応援をぜひよろしくお願いします🔥
いただいたチップでより素晴らしい記事を発信していけるよう精進してまいります!