基礎

Python例外処理入門|try-exceptでエラーを正しく処理する

Python 例外処理 エラーハンドリング

Python例外処理入門
try-exceptでエラーを正しく処理する

Pythonの例外処理の基本から、複数例外のキャッチ、finally、カスタム例外まで、エラーハンドリングの方法を解説します。

こんな人向けの記事です

  • Pythonの例外処理を基礎から学びたい
  • try-except-finally の使い方を理解したい
  • カスタム例外クラスを作成したい

Step 1例外処理の基本(try-except)

プログラムの実行中にエラーが発生すると、Pythonは例外(Exception)を送出してプログラムを停止します。例外処理を使えば、エラーが起きてもプログラムを止めずに適切に対処できます。

例外処理とは

「もしエラーが起きたら、こう対処する」という処理の流れを事前に定義しておく仕組みです。ユーザー入力やファイル操作など、予期しないエラーが起こりうる箇所で必須のテクニックです。

まずは例外処理を使わない場合の動作を確認しましょう。

Python(例外処理なし)
# 0で割り算するとエラーになる
result = 10 / 0
print(result)  # この行は実行されない

# 実行結果:
# ZeroDivisionError: division by zero

try-except を使うと、エラーをキャッチして処理を続行できます。

Python(try-exceptの基本)
try:
    result = 10 / 0
    print(result)
except ZeroDivisionError:
    print("0で割ることはできません")

print("プログラムは続行されます")

# 実行結果:
# 0で割ることはできません
# プログラムは続行されます

try ブロック内でエラーが発生すると、残りの処理をスキップして except ブロックに移動します。

例外オブジェクトを変数に格納して、エラーの詳細を取得することもできます。

Python(例外オブジェクトの取得)
try:
    number = int("abc")
except ValueError as e:
    print(f"エラーが発生しました: {e}")
    print(f"エラーの型: {type(e).__name__}")

# 実行結果:
# エラーが発生しました: invalid literal for int() with base 10: 'abc'
# エラーの型: ValueError
注意: すべての例外をキャッチしない

except:except Exception: で全例外をキャッチすると、バグの原因となるエラーまで隠蔽してしまいます。キャッチする例外は具体的に指定しましょう。

Step 2複数の例外をキャッチする

1つの try ブロックで、複数の異なる例外をそれぞれ処理することができます。

個別にキャッチする

Python(複数のexcept節)
def convert_and_divide(text, divisor):
    try:
        number = int(text)
        result = number / divisor
        return result
    except ValueError:
        print(f"'{text}' は数値に変換できません")
    except ZeroDivisionError:
        print("0で割ることはできません")
    except TypeError:
        print("不正な型が渡されました")

convert_and_divide("abc", 2)   # 'abc' は数値に変換できません
convert_and_divide("10", 0)    # 0で割ることはできません
convert_and_divide("10", None) # 不正な型が渡されました

複数の例外をまとめてキャッチする

同じ処理で対応できる例外は、タプルでまとめることができます。

Python(例外をまとめてキャッチ)
try:
    data = {"name": "太郎"}
    value = data["age"]
except (KeyError, IndexError) as e:
    print(f"データの取得に失敗しました: {e}")

# 実行結果:
# データの取得に失敗しました: 'age'

個別処理 + まとめてキャッチの組み合わせ

Python(組み合わせパターン)
def read_config(filepath):
    try:
        with open(filepath, "r") as f:
            config = f.read()
        value = int(config)
        return value
    except FileNotFoundError:
        print(f"ファイルが見つかりません: {filepath}")
    except PermissionError:
        print(f"ファイルの読み取り権限がありません: {filepath}")
    except (ValueError, TypeError) as e:
        print(f"設定値の変換に失敗: {e}")

read_config("missing.txt")  # ファイルが見つかりません: missing.txt
except節の順番

except節は上から順にマッチングされます。親クラスの例外を先に書くと、子クラスの例外がキャッチされなくなるので、具体的な例外を先に、汎用的な例外を後に書きましょう。

Step 3else節とfinally節

try-except には、elsefinally という2つの追加ブロックがあります。

ブロック実行タイミング用途
try常に実行を試みる例外が発生しうる処理
except例外が発生したときエラーへの対処
else例外が発生しなかったとき正常時のみ行う処理
finally例外の有無にかかわらず常にリソースの後片付け

else節

else ブロックは、try ブロックで例外が発生しなかった場合のみ実行されます。

Python(else節の使い方)
def safe_divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("0で割ることはできません")
    else:
        # 例外が起きなかったときだけ実行
        print(f"{a} / {b} = {result}")
        return result

safe_divide(10, 3)   # 10 / 3 = 3.3333333333333335
safe_divide(10, 0)   # 0で割ることはできません
なぜelse節を使うのか

try ブロックにすべてのコードを入れると、意図しないエラーまでキャッチしてしまう可能性があります。else に正常系の処理を分けることで、例外をキャッチする範囲を最小限にできます。

finally節

finally ブロックは、例外が起きても起きなくても必ず実行されます。リソースの解放(ファイルを閉じる、接続を切断する等)に使います。

Python(finally節の使い方)
def read_file(filepath):
    f = None
    try:
        f = open(filepath, "r")
        content = f.read()
        return content
    except FileNotFoundError:
        print(f"ファイルが見つかりません: {filepath}")
    finally:
        # 例外の有無に関わらず必ず実行
        if f is not None:
            f.close()
            print("ファイルを閉じました")

read_file("test.txt")

すべてを組み合わせた完全な形

Python(try-except-else-finally)
import json

def load_json(filepath):
    try:
        with open(filepath, "r") as f:
            data = json.load(f)
    except FileNotFoundError:
        print(f"ファイルが見つかりません: {filepath}")
        return None
    except json.JSONDecodeError as e:
        print(f"JSONの解析に失敗しました: {e}")
        return None
    else:
        print(f"正常に読み込みました({len(data)}件のデータ)")
        return data
    finally:
        print("--- load_json 処理完了 ---")

result = load_json("config.json")

# ファイルが存在しない場合:
# ファイルが見つかりません: config.json
# --- load_json 処理完了 ---
注意: finallyとreturn

finally ブロック内で return を使うと、tryexceptreturn を上書きしてしまいます。finally 内では return を使わないのがベストプラクティスです。

Step 4代表的な例外の種類

Pythonには多くの組み込み例外があります。よく遭遇するものを覚えておきましょう。

頻出する例外一覧

例外発生するケース
ValueError値が不正int("abc")
TypeError型が不正"2" + 3
KeyError辞書に存在しないキーd["missing"]
IndexErrorリストの範囲外アクセス[1,2,3][10]
FileNotFoundErrorファイルが存在しないopen("x.txt")
PermissionError権限がない読み取り専用ファイルへの書き込み
ZeroDivisionError0での除算10 / 0
AttributeError存在しない属性None.method()
ImportErrorインポート失敗import unknown_lib
StopIterationイテレータの末尾next(iter([]))

例外ごとの具体例

Python(ValueError)
# ValueError: 値の形式が不正
try:
    age = int("twenty")
except ValueError as e:
    print(f"ValueError: {e}")
# ValueError: invalid literal for int() with base 10: 'twenty'
Python(TypeError)
# TypeError: 型が一致しない操作
try:
    result = "10" + 5
except TypeError as e:
    print(f"TypeError: {e}")
# TypeError: can only concatenate str (not "int") to str
Python(KeyError / IndexError)
# KeyError: 辞書に存在しないキーへのアクセス
try:
    user = {"name": "太郎", "age": 25}
    email = user["email"]
except KeyError as e:
    print(f"KeyError: キー {e} が存在しません")
# KeyError: キー 'email' が存在しません

# IndexError: リストの範囲外へのアクセス
try:
    numbers = [1, 2, 3]
    value = numbers[10]
except IndexError as e:
    print(f"IndexError: {e}")
# IndexError: list index out of range
Python(FileNotFoundError)
# FileNotFoundError: ファイルが見つからない
try:
    with open("nonexistent.txt", "r") as f:
        content = f.read()
except FileNotFoundError as e:
    print(f"FileNotFoundError: {e}")
# FileNotFoundError: [Errno 2] No such file or directory: 'nonexistent.txt'

例外の階層構造

Pythonの例外はクラスの継承関係で階層化されています。

例外の継承階層(抜粋)
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
    ├── ValueError
    ├── TypeError
    ├── KeyError
    ├── IndexError
    ├── AttributeError
    ├── OSError
    │   ├── FileNotFoundError
    │   └── PermissionError
    ├── RuntimeError
    └── ArithmeticError
        └── ZeroDivisionError
例外の継承を意識する

except OSError と書くと、FileNotFoundErrorPermissionError もキャッチされます。より具体的な子クラスの例外を先にキャッチしましょう。

Step 5raiseで例外を発生させる

raise 文を使うと、プログラムの中で意図的に例外を発生させることができます。不正な入力を検出した場合や、処理を続行できない条件を明示するときに使います。

基本的な使い方

Python(raiseの基本)
def set_age(age):
    if not isinstance(age, int):
        raise TypeError("年齢は整数で指定してください")
    if age < 0:
        raise ValueError("年齢は0以上で指定してください")
    if age > 150:
        raise ValueError("年齢は150以下で指定してください")
    return age

# 使用例
try:
    set_age(-5)
except ValueError as e:
    print(f"エラー: {e}")
# エラー: 年齢は0以上で指定してください

try:
    set_age("二十歳")
except TypeError as e:
    print(f"エラー: {e}")
# エラー: 年齢は整数で指定してください

例外の再送出

キャッチした例外をログに記録した後、そのまま上位に伝播させたい場合は、引数なしの raise を使います。

Python(例外の再送出)
import logging

def process_data(data):
    try:
        result = int(data)
        return result * 2
    except ValueError:
        logging.error(f"不正なデータ: {data}")
        raise  # キャッチした例外をそのまま再送出

# 呼び出し側でもキャッチできる
try:
    process_data("abc")
except ValueError as e:
    print(f"処理に失敗しました: {e}")
# 処理に失敗しました: invalid literal for int() with base 10: 'abc'

例外チェーン(raise ... from)

元の例外を保持しつつ、別の例外を発生させることができます。デバッグ時に原因を追跡しやすくなります。

Python(例外チェーン)
class ConfigError(Exception):
    pass

def load_config(filepath):
    try:
        with open(filepath, "r") as f:
            return f.read()
    except FileNotFoundError as e:
        raise ConfigError(f"設定ファイルの読み込みに失敗: {filepath}") from e

try:
    config = load_config("settings.ini")
except ConfigError as e:
    print(f"ConfigError: {e}")
    print(f"原因: {e.__cause__}")
# ConfigError: 設定ファイルの読み込みに失敗: settings.ini
# 原因: [Errno 2] No such file or directory: 'settings.ini'
注意: raiseの使いどころ

raise は「この状態では処理を続行すべきでない」ことを明示するためのものです。通常の条件分岐で対処できるケースでは、例外ではなく if 文で処理しましょう。例外は例外的な状況に使うのが原則です。

Step 6カスタム例外クラスの作成

Pythonでは、Exception クラスを継承して独自の例外クラスを作成できます。アプリケーション固有のエラーを明確に区別できるようになります。

基本的なカスタム例外

Python(カスタム例外の基本)
# カスタム例外はExceptionを継承する
class InvalidEmailError(Exception):
    """不正なメールアドレスの例外"""
    pass

class UserNotFoundError(Exception):
    """ユーザーが見つからない例外"""
    pass

def validate_email(email):
    if "@" not in email:
        raise InvalidEmailError(f"不正なメールアドレス: {email}")

try:
    validate_email("invalid-email")
except InvalidEmailError as e:
    print(f"バリデーションエラー: {e}")
# バリデーションエラー: 不正なメールアドレス: invalid-email

属性を持つカスタム例外

エラーに関する追加情報を例外オブジェクトに持たせることができます。

Python(属性付きカスタム例外)
class ValidationError(Exception):
    def __init__(self, field, message, value=None):
        self.field = field
        self.message = message
        self.value = value
        super().__init__(f"{field}: {message}")

class AgeValidationError(ValidationError):
    pass

class NameValidationError(ValidationError):
    pass

def register_user(name, age):
    if not name or len(name.strip()) == 0:
        raise NameValidationError("name", "名前は必須です")
    if not isinstance(age, int) or age < 0:
        raise AgeValidationError("age", "年齢は0以上の整数です", value=age)
    return {"name": name, "age": age}

# 使用例
try:
    user = register_user("", 25)
except ValidationError as e:
    print(f"フィールド: {e.field}")
    print(f"エラー内容: {e.message}")
    print(f"不正な値: {e.value}")
# フィールド: name
# エラー内容: 名前は必須です
# 不正な値: None

例外の階層設計

実際のアプリケーションでは、例外クラスを階層的に設計すると便利です。

Python(例外の階層設計)
# アプリケーションの基底例外
class AppError(Exception):
    """アプリケーション共通の基底例外"""
    pass

# データベース関連の例外
class DatabaseError(AppError):
    pass

class ConnectionError(DatabaseError):
    pass

class QueryError(DatabaseError):
    pass

# 認証関連の例外
class AuthError(AppError):
    pass

class LoginFailedError(AuthError):
    pass

class PermissionDeniedError(AuthError):
    pass

# 使用例: 階層を利用した柔軟なキャッチ
def process_request():
    raise PermissionDeniedError("管理者権限が必要です")

try:
    process_request()
except AuthError as e:
    # LoginFailedError も PermissionDeniedError もキャッチ
    print(f"認証エラー: {e}")
except AppError as e:
    # その他のアプリケーションエラー
    print(f"アプリケーションエラー: {e}")
# 認証エラー: 管理者権限が必要です
カスタム例外の命名規則

カスタム例外クラスの名前は、末尾に Error をつけるのが慣例です(例: ValidationError, AuthError)。何のエラーなのか名前を見ただけで分かるようにしましょう。

実践的な例:ユーザー管理システム

Python(実践例)
class UserError(Exception):
    """ユーザー操作の基底例外"""
    pass

class UserNotFoundError(UserError):
    def __init__(self, user_id):
        self.user_id = user_id
        super().__init__(f"ユーザーID {user_id} は存在しません")

class DuplicateUserError(UserError):
    def __init__(self, email):
        self.email = email
        super().__init__(f"{email} は既に登録されています")

class UserManager:
    def __init__(self):
        self.users = {}

    def add_user(self, user_id, name, email):
        if email in [u["email"] for u in self.users.values()]:
            raise DuplicateUserError(email)
        self.users[user_id] = {"name": name, "email": email}
        return self.users[user_id]

    def get_user(self, user_id):
        if user_id not in self.users:
            raise UserNotFoundError(user_id)
        return self.users[user_id]

# 使用例
manager = UserManager()

try:
    manager.add_user(1, "太郎", "taro@example.com")
    manager.add_user(2, "花子", "taro@example.com")  # 重複
except DuplicateUserError as e:
    print(f"登録失敗: {e}")
# 登録失敗: taro@example.com は既に登録されています

try:
    user = manager.get_user(999)
except UserNotFoundError as e:
    print(f"取得失敗: {e}")
    print(f"検索したID: {e.user_id}")
# 取得失敗: ユーザーID 999 は存在しません
# 検索したID: 999

まとめ

  • try-except でエラーをキャッチしてプログラムの異常停止を防ぐ
  • 複数の except 節で例外ごとに異なる処理を実行できる
  • else は例外が起きなかったとき、finally は常に実行される
  • 組み込み例外の種類と階層構造を把握しておく
  • raise で意図的に例外を発生させ、不正な状態を明示する
  • カスタム例外で、アプリケーション固有のエラーをわかりやすく管理する