見出し画像

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 default


4. 例外を再送出する(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

いいなと思ったら応援しよう!

れん - Web Step | エンジニア技術配信 応援をぜひよろしくお願いします🔥 いただいたチップでより素晴らしい記事を発信していけるよう精進してまいります!