Python環境を起動中... 初回のみ数秒かかります
Lesson 17 / 20

例外処理

このレッスンで学ぶこと

  • try/except でエラーを捕捉してプログラムを継続できる
  • 複数の例外型を種類ごとに処理できる
  • else / finally の使い分けを理解できる
  • raise で意図的に例外を発生させ、カスタム例外クラスを作れる

1. try/except の基本

Pythonでは実行時に予期しないエラーが発生すると、何も対処しなければプログラムが強制終了(クラッシュ)します。 例えば int("N/A") を実行すると ValueError: invalid literal for int() with base 10: 'N/A' というエラーが発生し、以降の処理は一切実行されなくなります。

医療データは欠損値・不正値が混入することが日常的で、 「1件のエラーで処理全体が止まる」事態は許容できません。 try/except を使えばエラーを「捕まえて」適切に処理しながらプログラムを継続できます。

try ブロック内でエラーが発生すると処理が中断し、対応する except ブロックへ移ります。 except で捕捉されなかった例外はそのまま上位に伝播します。

sample_1.py
Ctrl+Enter
出力

💡 主な例外型: ValueError(不正な値)、TypeError(型の不一致)、KeyError(辞書に存在しないキー)、IndexError(リスト範囲外)、FileNotFoundError(ファイルなし)、ZeroDivisionError(0除算)

💡 try/except の実行フロー:

try ブロックを実行
 ├─ 例外が発生しなかった → try ブロック全体を実行して次へ進む
 └─ 例外が発生した     → その行で中断
      ├─ 対応する except がある → except ブロックを実行して次へ進む
      └─ 対応する except がない → 例外が上位に伝播(プログラムが止まる)

2. 複数の例外型を捕捉する

except 例外型 as e: と書くと、例外オブジェクトを変数 e に代入できます。 str(e) でエラーメッセージを、type(e).__name__ で例外クラス名を取得できるため、 エラーの詳細をログに残す・ユーザーにわかりやすく表示する、といった処理に役立ちます。

複数の except ブロックを書くことでエラーの種類ごとに異なる対処ができます。 except は上から順に評価されるため、具体的な例外を先に、 汎用的な Exception を後ろに書くのが基本ルールです。 逆にすると、上位の except Exception がすべてのエラーを吸収してしまい、 下の except KeyError などには一切到達しなくなります。

sample_2.py
Ctrl+Enter
出力

💡 except の書き方バリエーション:

except ValueError:               # 単一の例外型を捕捉
except (ValueError, TypeError):  # 複数をまとめて捕捉(どちらでも同じ処理)
except ValueError as e:          # 例外オブジェクトを e に代入
except Exception as e:           # すべての例外を捕捉(最後の手段)
except:                          # 例外型不問(SystemExit も捕捉:非推奨)

except Exception as e: は「想定外のエラーが起きたらログを記録して終了する」 用途で最後のフォールバックとして使います。ただし乱用するとバグを隠す原因になるため注意が必要です。

💡 例外オブジェクト e から取れる情報:

try:
    value = int("N/A")
except ValueError as e:
    print(str(e))             # → invalid literal for int() with base 10: 'N/A'
    print(type(e).__name__)   # → ValueError
    print(e.args)             # → ("invalid literal for int() with base 10: 'N/A'",)

ログファイルにエラー詳細を記録したい場合は str(e) でメッセージ文字列を取得できます。

3. else と finally

else ブロックは、try 内で例外が発生しなかった場合のみ実行されます。 「成功後の処理を try の中に書けばよいのでは?」と思うかもしれません。 しかし、それをすると気づきにくいバグを生む可能性があります。

🐛 try に書きすぎると起きる問題:

# NG 問題のある書き方
try:
    value = int(user_input)     # ← 入力変換(ここの ValueError を捕まえたい)
    save_to_db(value)           # ← DB保存(ここの ValueError も同じ except に来てしまう!)
except ValueError:
    print("数値を入力してください")  # DB保存のバグでも「数値を入力して」と表示されてしまう

save_to_db() の内部でも ValueError が発生すると、 入力は正しかったのに「数値を入力してください」と表示されます。 本当の原因(DBエラー)は隠れてしまいます。

# OK else を使った書き方
try:
    value = int(user_input)     # ← ここだけを監視
except ValueError:
    print("数値を入力してください")  # int() のエラーだけを捕捉
else:
    save_to_db(value)           # try が成功したときだけ実行。ここの ValueError は上の except に吸収されない

else に書いた処理で例外が起きた場合は except をスルーして プログラムの外側へ伝播します。つまり「隠さず、そのままエラーを出す」動作になり、 バグの原因がすぐに分かります。

finally ブロックは例外の有無にかかわらず必ず実行されます。 ファイルのクローズ・データベース接続の解放・ログの記録など、 「何があっても確実に実行したい」クリーンアップ処理に使います。 returnbreak で途中終了した場合でも finally は実行される点が重要です。

sample_3.py
Ctrl+Enter
出力

💡 try / except / else / finally の実行フロー:

シナリオ try except else finally
例外なし ✅ 全体
例外あり(捕捉) ⛔ 途中
例外あり(未捕捉) ⛔ 途中 ✅ → 伝播

💡 実際はwith文を使う: ファイルを with open(...) で開けば finally での手動クローズは不要です。with 文は内部的に __enter__ / __exit__ を使って自動クリーンアップを行います。finally はDB接続のクローズや一時ファイルの削除など、with 構文が使えない場面で真価を発揮します。

💡 finally が役立つ実用例:

  • データベース接続の解放(conn.close() を必ず実行)
  • ネットワークソケットのクローズ
  • 処理の開始・終了ログの記録(成功・失敗にかかわらず処理完了を記録)
  • 一時ファイルの削除(エラーで中断してもゴミファイルを残さない)
  • プログレスバーのリセット(UI処理でエラー後も画面を正常状態に戻す)

4. raise

raise 例外型("メッセージ") を使うと、条件を満たさない入力に対して 意図的に例外を発生させられます。例えば年齢が負の値、SpO2 が 100 を超える値など、 医学的にあり得ない値を関数に渡された場合、早期にエラーを発生させることで バグの原因を明確にできます(フェイル・ファストの原則)。 問題が発生した箇所と原因が一致するため、デバッグが格段に楽になります。

sample_4.py
Ctrl+Enter
出力

💡 raise と try/except の役割分担:

  • 関数の中(raise):「この値はおかしい」と例外を投げるだけ。どう対処するかは関知しない
  • 呼び出し元(try/except):投げられた例外を受け取る。エラーメッセージを表示したり、デフォルト値を使うなど対処を決める
validate_age(-5) を呼び出す
  → 関数内で raise ValueError(...) が実行される
  → 例外が呼び出し元の try ブロックに伝わる
  → except ValueError as e: で受け取る  ← ここがなければプログラム停止

💡 raise(引数なし)— 例外を受け取りつつ上位に投げ直す:

except ブロック内で raise だけ書くと、受け取った例外をそのまま上位に再発生させます。 「エラーをログに記録したいが、例外を握りつぶさず呼び出し元にも伝えたい」場面で使います。

try:
    validate_age(-5)
except ValueError as e:
    print(f"[ログ] エラーを記録: {e}")  # ログだけ残して…
    raise                               # 例外を再び投げる → 呼び出し元にも伝わる

raise を書かずに except ブロックを終わらせると例外は握りつぶされ、呼び出し元はエラーが起きたことを知れません。意図的にエラーを無視するのでなければ、raise で伝播させるのが基本です。

5. カスタム例外

カスタム例外とは、標準の例外クラス(ValueError など)を継承して作る独自の例外クラスです。 class InvalidVitalError(ValueError): pass のように1行書くだけで定義できます。

ValueError をそのまま使っても動きますが、ValueErrorint("abc") のような型変換ミスでも発生します。 except ValueError と書いたとき、「バイタル値が不正」なのか「型変換に失敗」なのか区別できません。 カスタム例外を使えば except InvalidVitalError で「バイタル値の問題だけ」を確実に捕捉でき、 例外名自体が「何のエラーか」を説明するため、コードの意図も伝わりやすくなります。

sample_5.py
Ctrl+Enter
出力

💡 カスタム例外クラスの設計指針:

  • クラス名は Error で終わる慣習(例: InvalidVitalErrorDataValidationError
  • 適切な標準例外を継承する(入力値の不正 → ValueError、実行時の異常 → RuntimeError
  • シンプルな用途なら pass だけでクラスを作ればOK。__init__ は追加情報が必要なときだけ実装する
  • PyTorchの RuntimeError: CUDA out of memoryValueError: Expected input size なども同じ仕組みで定義されている

6. 練習問題

問題 1

型変換のエラーハンドリング

文字列のリストを整数に変換する関数 parse_values(str_list) を作ってください。変換できない要素は None に置き換え、最後に変換成功・失敗の件数を表示します。

exercise_1.py
出力
ヒントを見る(答え+解説)
def parse_values(str_list):
    results = []
    success = 0
    fail = 0
    for s in str_list:
        try:
            results.append(int(s))
            success += 1
        except ValueError:
            results.append(None)
            fail += 1
    print(f"成功: {success}件, 失敗: {fail}件")
    return results

data = ["120", "N/A", "98", "--", "155", "200", "不明"]
result = parse_values(data)
print(result)
# → 成功: 4件, 失敗: 3件
# → [120, None, 98, None, 155, 200, None]

try ブロックで変換を試み、失敗した場合は except ValueError で捕捉します。変換できない文字列を None に置き換えるパターンは、医療データの前処理でよく使います。

問題 2

年齢バリデーションで raise を使う

年齢が0未満または150を超える場合に ValueError を発生させる関数 validate_age(age) を作ってください。正常な場合は True を返します。

exercise_2.py
出力
ヒントを見る(答え+解説)
def validate_age(age):
    if age < 0:
        raise ValueError(f"年齢は0以上である必要があります: {age}")
    if age > 150:
        raise ValueError(f"年齢は150以下である必要があります: {age}")
    return True

test_ages = [25, -5, 80, 200, 0]
for age in test_ages:
    try:
        if validate_age(age):
            print(f"✅ {age}歳: 有効")
    except ValueError as e:
        print(f"❌ {age}歳: {e}")
# → ✅ 25歳: 有効 / ❌ -5歳: ... / ✅ 80歳: 有効 / ❌ 200歳: ...

raise ValueError("メッセージ") で意図的に例外を発生させます。呼び出し側で except ValueError as e と受け取ることで、バリデーションエラーを適切に処理できます。

問題 3

finally でクリーンアップ

以下の関数に finally ブロックを追加し、例外の有無にかかわらず「処理完了ログを記録しました」と表示してください。

exercise_3.py
出力
ヒントを見る(答え+解説)
def process_data(data):
    try:
        result = 100 / data
        print(f"処理結果: {result:.2f}")
        return result
    except ZeroDivisionError:
        print("エラー: ゼロ除算が発生しました")
        return None
    finally:
        print("処理完了ログを記録しました")

process_data(5)
print()
process_data(0)
# → 処理結果: 20.00 / 処理完了ログを記録しました
# → エラー: ゼロ除算が発生しました / 処理完了ログを記録しました

finally ブロックは例外の有無にかかわらず必ず実行されます。ログ記録・ファイルのクローズ・DB接続の解放など、確実に実行したいクリーンアップ処理に使います。

まとめ

このレッスンのポイント

  • try/except でエラーを捕捉してプログラムのクラッシュを防ぐ
  • except 例外型 as e: で例外オブジェクトを取得し、詳細を確認
  • else: 例外がなかった場合のみ実行(成功時の後処理)
  • finally: 例外の有無にかかわらず必ず実行(クリーンアップ)
  • raise 例外型("メッセージ") で意図的に例外を投げる
  • class MyError(Exception): でカスタム例外を定義できる

自由に試してみましょう:

free_practice.py
Ctrl+Enter
出力

完了するとトップページに進捗が表示されます