Python例外処理入門
try-exceptでエラーを正しく処理する
Pythonの例外処理の基本から、複数例外のキャッチ、finally、カスタム例外まで、エラーハンドリングの方法を解説します。
こんな人向けの記事です
- Pythonの例外処理を基礎から学びたい
- try-except-finally の使い方を理解したい
- カスタム例外クラスを作成したい
Step 1例外処理の基本(try-except)
プログラムの実行中にエラーが発生すると、Pythonは例外(Exception)を送出してプログラムを停止します。例外処理を使えば、エラーが起きてもプログラムを止めずに適切に対処できます。
「もしエラーが起きたら、こう対処する」という処理の流れを事前に定義しておく仕組みです。ユーザー入力やファイル操作など、予期しないエラーが起こりうる箇所で必須のテクニックです。
まずは例外処理を使わない場合の動作を確認しましょう。
# 0で割り算するとエラーになる
result = 10 / 0
print(result) # この行は実行されない
# 実行結果:
# ZeroDivisionError: division by zero
try-except を使うと、エラーをキャッチして処理を続行できます。
try:
result = 10 / 0
print(result)
except ZeroDivisionError:
print("0で割ることはできません")
print("プログラムは続行されます")
# 実行結果:
# 0で割ることはできません
# プログラムは続行されます
try ブロック内でエラーが発生すると、残りの処理をスキップして except ブロックに移動します。
例外オブジェクトを変数に格納して、エラーの詳細を取得することもできます。
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 ブロックで、複数の異なる例外をそれぞれ処理することができます。
個別にキャッチする
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) # 不正な型が渡されました
複数の例外をまとめてキャッチする
同じ処理で対応できる例外は、タプルでまとめることができます。
try:
data = {"name": "太郎"}
value = data["age"]
except (KeyError, IndexError) as e:
print(f"データの取得に失敗しました: {e}")
# 実行結果:
# データの取得に失敗しました: 'age'
個別処理 + まとめてキャッチの組み合わせ
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節は上から順にマッチングされます。親クラスの例外を先に書くと、子クラスの例外がキャッチされなくなるので、具体的な例外を先に、汎用的な例外を後に書きましょう。
Step 3else節とfinally節
try-except には、else と finally という2つの追加ブロックがあります。
| ブロック | 実行タイミング | 用途 |
|---|---|---|
try | 常に実行を試みる | 例外が発生しうる処理 |
except | 例外が発生したとき | エラーへの対処 |
else | 例外が発生しなかったとき | 正常時のみ行う処理 |
finally | 例外の有無にかかわらず常に | リソースの後片付け |
else節
else ブロックは、try ブロックで例外が発生しなかった場合のみ実行されます。
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で割ることはできません
try ブロックにすべてのコードを入れると、意図しないエラーまでキャッチしてしまう可能性があります。else に正常系の処理を分けることで、例外をキャッチする範囲を最小限にできます。
finally節
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")
すべてを組み合わせた完全な形
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 を使うと、try や except の return を上書きしてしまいます。finally 内では return を使わないのがベストプラクティスです。
Step 4代表的な例外の種類
Pythonには多くの組み込み例外があります。よく遭遇するものを覚えておきましょう。
頻出する例外一覧
| 例外 | 発生するケース | 例 |
|---|---|---|
ValueError | 値が不正 | int("abc") |
TypeError | 型が不正 | "2" + 3 |
KeyError | 辞書に存在しないキー | d["missing"] |
IndexError | リストの範囲外アクセス | [1,2,3][10] |
FileNotFoundError | ファイルが存在しない | open("x.txt") |
PermissionError | 権限がない | 読み取り専用ファイルへの書き込み |
ZeroDivisionError | 0での除算 | 10 / 0 |
AttributeError | 存在しない属性 | None.method() |
ImportError | インポート失敗 | import unknown_lib |
StopIteration | イテレータの末尾 | next(iter([])) |
例外ごとの具体例
# ValueError: 値の形式が不正
try:
age = int("twenty")
except ValueError as e:
print(f"ValueError: {e}")
# ValueError: invalid literal for int() with base 10: 'twenty'
# TypeError: 型が一致しない操作
try:
result = "10" + 5
except TypeError as e:
print(f"TypeError: {e}")
# TypeError: can only concatenate str (not "int") to str
# 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
# 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 と書くと、FileNotFoundError や PermissionError もキャッチされます。より具体的な子クラスの例外を先にキャッチしましょう。
Step 5raiseで例外を発生させる
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 を使います。
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)
元の例外を保持しつつ、別の例外を発生させることができます。デバッグ時に原因を追跡しやすくなります。
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 は「この状態では処理を続行すべきでない」ことを明示するためのものです。通常の条件分岐で対処できるケースでは、例外ではなく if 文で処理しましょう。例外は例外的な状況に使うのが原則です。
Step 6カスタム例外クラスの作成
Pythonでは、Exception クラスを継承して独自の例外クラスを作成できます。アプリケーション固有のエラーを明確に区別できるようになります。
基本的なカスタム例外
# カスタム例外は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
属性を持つカスタム例外
エラーに関する追加情報を例外オブジェクトに持たせることができます。
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
例外の階層設計
実際のアプリケーションでは、例外クラスを階層的に設計すると便利です。
# アプリケーションの基底例外
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)。何のエラーなのか名前を見ただけで分かるようにしましょう。
実践的な例:ユーザー管理システム
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で意図的に例外を発生させ、不正な状態を明示する- カスタム例外で、アプリケーション固有のエラーをわかりやすく管理する