<一覧に戻る

例外処理の工夫

プログラムを書いていて「動くはずなのに、なぜか止まってしまった…」という経験はありませんか?

Pythonでは、実行時に発生するエラー(例外)を上手に扱うことで、アプリが突然落ちるのを防ぎ、ユーザーに優しいメッセージを返せるようになります。ここでは、実務でも役立つ例外処理の考え方とテクニックを丁寧に解説します。try/exceptの基本からログ出力、再スロー、カスタム例外まで、ステップを追って身につけていきましょう。

基本的な例外処理

まずは。例外処理のtry/except/else/finallyの流れを体験してみましょう。どんなときに例外が起きて、どのように受け止めるのかがわかると、エラーが怖くなくなります。

def safe_divide():
    try:
        a = int(input("割られる数(整数)を入力してください: "))
        b = int(input("割る数(整数)を入力してください: "))
        result = a / b
    except ValueError:
        # 入力を整数に変換できなかった場合
        print("無効な入力です。整数を入力してください。")
    except ZeroDivisionError:
        # 0で割ろうとした場合
        print("0では割れません。別の数を入力してください。")
    else:
        # 例外が一度も起きなかったときだけ実行
        print(f"{a} ÷ {b} = {result}")
    finally:
        # 成功・失敗に関係なく必ず実行
        print("処理を終了します。")

safe_divide()

このコードでは、ユーザー入力を整数に変換し、割り算をする処理をtryの中にまとめています。

もし文字を入力してしまったらValueError、割る数が0ならZeroDivisionErrorが発生し、それぞれのexceptでわかりやすいメッセージを返します。問題なく計算できた場合はelseで結果を表示し、最後にfinallyで「処理を終了します」と必ず締めるようにしています。elseとfinallyを併用すると、正常系と片付け処理(後始末)をきれいに分けられるのがポイントです。

例外のログ出力

「その場ではメッセージを出したけど、あとから原因を調べたい」と感じたことはありませんか?

運用やデバッグを考えると、例外をログとして残しておくのが重要です。printだけでは流れてしまう情報も、ログに書けば後から振り返れます。

import logging

# ロガーの基本設定(ファイルへ出力、タイムスタンプ付き)
logging.basicConfig(
    filename="error.log",
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger("calculator")

def safe_divide_with_logging():
    try:
        a = int(input("割られる数(整数)を入力してください: "))
        b = int(input("割る数(整数)を入力してください: "))
        result = a / b
    except ValueError as ve:
        # ユーザー向けにはやさしい文言、ログには詳細
        logger.warning(f"入力変換エラー: {ve}")
        print("無効な入力です。整数を入力してください。")
    except ZeroDivisionError as zde:
        logger.error(f"0除算エラー: {zde}")
        print("0では割れません。別の数を入力してください。")
    except Exception:
        # 予期しないエラーはスタックトレースごと残す
        logger.exception("想定外のエラーが発生しました")
        print("予期しないエラーが発生しました。時間をおいて再度お試しください。")
    else:
        logger.info(f"計算成功: {a} / {b} = {result}")
        print(f"{a} ÷ {b} = {result}")

safe_divide_with_logging()

ここでのポイントは、ユーザーに見せるメッセージと、開発者が読むログを分けていることです。ValueErrorやZeroDivisionErrorのように想定できるエラーはログレベルをwarningやerrorに、想定外のエラーはlogger.exceptionでスタックトレース込みの情報を残します。これにより、ユーザー体験を損なわずに、原因追跡に十分な情報を確保できます。

例外を再発生させる(再スロー)

ときには「ここでいったん握ってユーザーに伝えるが、上位の処理にも知らせたい」という状況があります。そんなときは例外を再スロー(re-raise)します。握りつぶさずに、伝えるべき層にきちんと届けることが大切です。

def ask_and_divide():
    try:
        a = int(input("割られる数(整数)を入力してください: "))
        b = int(input("割る数(整数)を入力してください: "))
        return a / b
    except ValueError as ve:
        print("入力に誤りがあります。整数を入力してください。")
        # 文言を整えて上位に伝える。元の原因は維持する
        raise ValueError("ユーザー入力を整数に変換できませんでした。") from ve
    except ZeroDivisionError:
        print("0では割れません。別の数を入力してください。")
        # 原因を変えずにそのまま再スロー
        raise

try:
    result = ask_and_divide()
    print(f"結果: {result}")
except Exception as e:
    # ここで一括して集約的に処理したり、ログに残したりできる
    print(f"上位で例外を受け取りました: {e}")

再スローには2パターンあります。raiseだけなら同じ例外をそのまま上に投げます。 raise 新しい例外 from 元の例外と書くと、「何が原因でこの例外になったのか」という因果関係(例外チェーン)を保ったまま、よりわかりやすい文脈で伝えられます。大規模な処理では、ここが原因追跡の明暗を分けます。

カスタム例外の定義

このアプリだけのルール違反」を明確に伝えたいときは、独自の例外(カスタム例外)を作ると管理しやすくなります。標準の例外を継承し、意味を持たせるのがコツです。

class NegativeNumberError(ValueError):
    """負の数が不正な文脈で使われたことを表すアプリ固有の例外"""
    pass

def half_of_positive_int():
    try:
        n = int(input("0以上の整数を入力してください: "))
        if n < 0:
            # アプリのルールに違反したときに、はっきりと伝える
            raise NegativeNumberError("負の数は許可されていません。0以上を入力してください。")
        result = n / 2
        print(f"{n} の半分は {result} です。")
    except NegativeNumberError as ne:
        print(ne)
    except ValueError:
        print("無効な入力です。整数を入力してください。")
    except Exception as e:
        print(f"予期しないエラーが発生しました: {e}")

half_of_positive_int()

ここではValueErrorを継承したNegativeNumberErrorを定義し、「負の数はこの関数では不正」という意図をコードに刻みました。

カスタム例外を使うと、except NegativeNumberErrorだけを狙い撃ちで捕まえられるので、エラーごとのハンドリングがすっきりします。将来、別の入力ルールを追加しても拡張しやすくなるのも利点です。

さらに使いこなすためのヒント

ここまで読んで「なんとなく分かってきたけれど、実務ではどう使い分ければいい?」と感じた方のために小さなコツをまとめます。

  • 【まず具体的な例外から書く】 exceptの順序は「より具体的 → より一般的」。Exceptionを先に書くと、他が届きません。
  • 【例外は握りつぶさない】 exceptで何もせずpassはNG。ユーザー向けメッセージとログのどちらか(できれば両方)を残しましょう。
  • 【ユーザー向け文言とログは分ける】 ユーザーには簡潔でやさしい言葉、ログには原因特定に必要な詳細とスタックトレースを。
  • 【EAFPを意識する】 「先にチェック(LBYL)」より「まずやってみて、ダメならexcept(EAFP)」がPython的で読みやすいことが多いです。
  • 【リソースの後始末はfinallyやwithで】 ファイルやネットワークなどのクリーンアップはfinally、またはwith文で安全に。

まとめ

Pythonの例外処理は、ただ「落ちないようにする」ためだけの仕組みではありません。

ユーザー体験を守り、ログで原因を記録し、必要に応じて上位に伝える——この一連の流れを設計できると、コードはぐっと信頼性の高いものになります。try/except/else/finallyの基本、loggingによる記録、raiseやraise fromの再スロー、そしてカスタム例外の活用。どれも難しく見えて、実は一つひとつのステップはシンプルです。まずは今日紹介したサンプルをそのまま動かし、少しずつ自分のアプリに取り入れてみましょう。

次にエラーが起きたとき、「チャンスだ、よい設計に直せる」と前向きに思えるはずです。

出力結果: