2.2.2 例外処理

このページを終えたら、この evidence card を残します。
- パターン
- class、exception、file IO、functional pipeline、generator、またはtype hint
- コード成果物
- 最小限の実行可能な例と、現実的なユースケース 1 つ
- 出力
- 印字されたオブジェクト状態、捕捉したエラー、保存したファイル、yieldされた値、または型チェックのメモ
- 失敗確認
- 隠れた変更、副作用を飲み込む例外、ファイルパスの問題、lazy iterator の混同、または誤解を招く注釈
- 期待される成果
- デバッグメモを含む小さな高度Python例
この節の位置づけ
Section titled “この節の位置づけ”この節では、エラーが起きてもプログラムがすぐにクラッシュしないようにします。例外処理は、ファイルの読み書き、ネットワークリクエスト、API 呼び出し、データクリーニング、モデル推論で何度も登場します。ここでは、エラーを事前に予測し、捕捉し、復旧できる形で対処することを学びます。
- 例外とは何か、なぜ例外処理が必要なのかを理解する
try/except/else/finallyの使い方を身につける- 異なる種類の例外を捕捉できるようになる
- すぐにクラッシュしない、堅牢なプログラムを書けるようになる
例外とは、プログラム実行中に起きるエラーのことです。例外処理がないプログラムは、エラーが起きるとそのままクラッシュします。
# これらのコードはすべてプログラムをクラッシュさせますprint(10 / 0) # ZeroDivisionError: ゼロ除算print(int("abc")) # ValueError: 変換できないprint([1, 2, 3][10]) # IndexError: インデックスが範囲外print({"a": 1}["b"]) # KeyError: キーが存在しない
# プログラムがクラッシュすると、この後のコードは実行されませんprint("この行は絶対に実行されません")実際のプログラムでは、エラーは避けられません。ユーザーが不正なデータを入力することもありますし、ファイルが存在しないこともあります。ネットワークが切れることもあります。例外処理を使うと、こうした問題に丁寧に対応でき、プログラムを直接クラッシュさせずに済みます。
よくある例外の種類
Section titled “よくある例外の種類”| 例外の種類 | 発生する場面 | 例 |
|---|---|---|
ZeroDivisionError | ゼロ除算 | 1 / 0 |
TypeError | 型が合わない操作 | "hello" + 5 |
ValueError | 値が不正 | int("abc") |
IndexError | リストのインデックスが範囲外 | [1, 2][5] |
KeyError | 辞書にキーが存在しない | {"a": 1}["b"] |
FileNotFoundError | ファイルが存在しない | open("存在しない.txt") |
AttributeError | 属性が存在しない | "hello".foo() |
NameError | 変数が定義されていない | print(xyz) |
ImportError | import に失敗する | import 存在しないモジュール |
try / except の基本
Section titled “try / except の基本”try/except の流れは、まずコードを試し、エラーが起きたら代わりの処理を実行する、というものです。
try: number = int(input("数字を入力してください: ")) print(f"入力された数字: {number}")except ValueError: print("入力が無効です!数字を入力してください。")
print("プログラムは続行します...") # 例外があってもなくても、この行は実行されます実行例:
| 入力 | 何が起きるか |
|---|---|
42 | 入力された数字: 42 を表示し、その後も続行する。 |
abc | 入力エラーのメッセージを表示し、その後も続行する。 |
ポイントは、try/except があれば、エラーが起きてもプログラムはクラッシュしないことです。
異なる種類の例外を捕捉する
Section titled “異なる種類の例外を捕捉する”複数の例外をそれぞれ捕捉する
Section titled “複数の例外をそれぞれ捕捉する”def safe_divide(a, b): try: result = a / b return result except ZeroDivisionError: print("エラー:ゼロで割ることはできません!") return None except TypeError: print("エラー:数値を渡してください!") return None
print(safe_divide(10, 3)) # 3.333...print(safe_divide(10, 0)) # エラー:ゼロで割ることはできません! → Noneprint(safe_divide("10", 3)) # エラー:数値を渡してください! → None複数の例外をまとめて捕捉する
Section titled “複数の例外をまとめて捕捉する”try: # エラーが起きる可能性があるコード value = int(input("数字を入力してください: ")) result = 100 / value print(f"結果: {result}")except (ValueError, ZeroDivisionError) as e: print(f"エラーが発生しました: {e}")例外情報を取得する
Section titled “例外情報を取得する”try: number = int("abc")except ValueError as e: print(f"例外の種類: {type(e).__name__}") # ValueError print(f"例外メッセージ: {e}") # invalid literal for int() with base 10: 'abc'すべての例外を捕捉する(注意して使う)
Section titled “すべての例外を捕捉する(注意して使う)”try: # いくつかのコード result = risky_operation()except Exception as e: print(f"予期しないエラーが発生しました: {type(e).__name__}: {e}")try / except / else / finally
Section titled “try / except / else / finally”完全な例外処理の構造は、次の4つの部分からなります。
try: # 試して実行するコード file = open("data.txt", "r") content = file.read()except FileNotFoundError: # エラーが起きたときに実行 print("ファイルが存在しません!")else: # エラーがなかったときに実行 print(f"ファイルの内容: {content}")finally: # エラーの有無にかかわらず実行(通常はリソースの片付けに使う) print("処理が完了しました")| 節 | 実行されるタイミング | 用途 |
|---|---|---|
try | いつも実行 | エラーが起きるかもしれないコードを置く |
except | エラーが起きたときだけ実行 | エラーを処理する |
else | エラーが起きなかったときだけ実行 | 成功後の処理を置く |
finally | エラーの有無にかかわらず実行 | リソースの片付け(ファイルを閉じる、接続を切る) |
finally の典型的な使い方
Section titled “finally の典型的な使い方”file = Nonetry: file = open("data.txt", "r") data = file.read() # データを処理...except FileNotFoundError: print("ファイルが存在しません")finally: if file: file.close() # エラーの有無にかかわらず、ファイルは閉じる print("ファイルを閉じました")例外を投げる
Section titled “例外を投げる”例外を処理するだけでなく、自分で例外を投げることもできます。これは、ありえない状態や不正な状態を見つけたときに、呼び出し元へ「問題がある」と伝えるためです。
raise 文
Section titled “raise 文”def set_age(age): if not isinstance(age, int): raise TypeError("年齢は整数でなければなりません") if age < 0 or age > 150: raise ValueError(f"年齢 {age} は不適切です。0〜150 の範囲である必要があります") return age
# 正常な使用print(set_age(25)) # 25
# 例外を発生させるtry: set_age(-5)except ValueError as e: print(f"エラー: {e}") # エラー: 年齢 -5 は不適切です。0〜150 の範囲である必要があります
try: set_age("二十")except TypeError as e: print(f"エラー: {e}") # エラー: 年齢は整数でなければなりません独自の例外を作る
Section titled “独自の例外を作る”組み込みの例外では足りない場合は、自分で定義できます。
class InsufficientFundsError(Exception): """残高不足の例外""" def __init__(self, balance, amount): self.balance = balance self.amount = amount super().__init__(f"残高不足:現在の残高は {balance}、引き出そうとした金額は {amount} です")
class BankAccount: def __init__(self, balance=0): self.balance = balance
def withdraw(self, amount): if amount > self.balance: raise InsufficientFundsError(self.balance, amount) self.balance -= amount return self.balance
# 使用例account = BankAccount(1000)try: account.withdraw(1500)except InsufficientFundsError as e: print(f"取引失敗: {e}") print(f"現在の残高: {e.balance}, 依頼金額: {e.amount}")実践パターン
Section titled “実践パターン”パターン 1:LBYL と EAFP
Section titled “パターン 1:LBYL と EAFP”Python コミュニティでは、EAFP(Easier to Ask Forgiveness than Permission、先にやってから考える)が、LBYL(Look Before You Leap、先に確認してから実行する)より好まれます。
# LBYL スタイル(先に確認してから操作)—— Python らしくないif key in my_dict: value = my_dict[key]else: value = default_value
# EAFP スタイル(先に操作し、エラーが起きたら処理)—— より Python らしいtry: value = my_dict[key]except KeyError: value = default_value
# もちろん、辞書にはもっとよい書き方もありますvalue = my_dict.get(key, default_value)パターン 2:再試行メカニズム
Section titled “パターン 2:再試行メカニズム”import time
def fetch_data_with_retry(url, max_retries=3): """再試行付きでデータを取得する""" for attempt in range(1, max_retries + 1): try: print(f"{attempt} 回目の試行...") # ネットワークリクエストを模擬 import random if random.random() < 0.5: raise ConnectionError("ネットワーク接続に失敗しました") return "取得したデータ" except ConnectionError as e: print(f" 失敗: {e}") if attempt < max_retries: wait = attempt * 2 # 待ち時間を徐々に長くする print(f" {wait} 秒後に再試行します...") time.sleep(wait) else: print(" すべての再試行に失敗しました!") raise # 最後の再試行も失敗したら例外を投げる
try: data = fetch_data_with_retry("https://api.example.com") print(f"成功: {data}")except ConnectionError: print("最終的にデータ取得に失敗しました")パターン 3:安全なユーザー入力
Section titled “パターン 3:安全なユーザー入力”def get_number(prompt, min_val=None, max_val=None): """ユーザー入力の数字を安全に取得する""" while True: try: value = float(input(prompt)) if min_val is not None and value < min_val: print(f"{min_val} 以上の数を入力してください") continue if max_val is not None and value > max_val: print(f"{max_val} 以下の数を入力してください") continue return value except ValueError: print("有効な数字を入力してください!")
# 使用例age = get_number("年齢を入力してください: ", min_val=0, max_val=150)print(f"あなたの年齢は: {age}")総合例:安全なタスク見積もり管理
Section titled “総合例:安全なタスク見積もり管理”class TaskEstimateManager: def __init__(self): self.tasks = {}
def add_task(self, name, hours): """タスク見積もりを追加する""" if not isinstance(name, str) or not name.strip(): raise ValueError("タスク名は空にできません") if not isinstance(hours, (int, float)): raise TypeError(f"時間は数値である必要があります。受け取った型: {type(hours).__name__}") if not 0 <= hours <= 80: raise ValueError(f"時間 {hours} は範囲外です(0〜80)")
self.tasks[name] = hours print(f"✅ 追加成功: {name} - {hours} 時間")
def get_average_hours(self): """平均見積もりを取得する""" if not self.tasks: raise RuntimeError("タスクデータがないため、平均を計算できません") return sum(self.tasks.values()) / len(self.tasks)
def get_task(self, name): """タスク見積もりを検索する""" if name not in self.tasks: raise KeyError(f"タスクが見つかりません: {name}") return self.tasks[name]
# 使用manager = TaskEstimateManager()
# タスク見積もりを安全に追加するtest_data = [ ("ログイン API", 8), ("RAG デモ", 12), ("グラフビュー", "すぐ"), # 型エラー ("データ取り込み", 120), # 範囲エラー ("", 6), # 名前が空 ("デプロイスクリプト", 5),]
for name, hours in test_data: try: manager.add_task(name, hours) except (ValueError, TypeError) as e: print(f"❌ 追加失敗: {e}")
# 検索print(f"\n平均見積もり: {manager.get_average_hours():.1f} 時間")
try: print(manager.get_task("決済フロー"))except KeyError as e: print(f"検索失敗: {e}")手を動かしてみよう
Section titled “手を動かしてみよう”練習 1:安全な計算機
Section titled “練習 1:安全な計算機”def safe_calculator(inputs=None): """不正入力とゼロ除算を処理できる、安全な四則演算機。""" inputs = iter(inputs or ["10", "0", "/", "n"])
while True: try: a = float(next(inputs) if inputs else input("1つ目の数値: ")) b = float(next(inputs) if inputs else input("2つ目の数値: ")) op = next(inputs) if inputs else input("演算子(+、-、*、/): ")
if op == "+": result = a + b elif op == "-": result = a - b elif op == "*": result = a * b elif op == "/": result = a / b else: raise ValueError(f"未対応の演算子です: {op}")
print(f"結果: {result}") except ZeroDivisionError: print("ゼロで割ることはできません。") except ValueError as error: print(f"入力が不正です: {error}") except StopIteration: break
again = next(inputs, "n") if inputs else input("続けますか?(y/n): ") if again.lower() != "y": break
safe_calculator()練習 2:ファイル読み取り器
Section titled “練習 2:ファイル読み取り器”def read_file_safely(filename): """ファイルの内容を安全に読み取る。""" try: with open(filename, "r", encoding="utf-8") as file: return file.read() except FileNotFoundError: print(f"ファイルが見つかりません: {filename}") except PermissionError: print(f"読み取り権限がありません: {filename}") except OSError as error: print(f"読み取りに失敗しました: {error}") return None
content = read_file_safely("test.txt")if content: print(content)練習 3:一括型変換
Section titled “練習 3:一括型変換”def convert_to_numbers(data_list): """文字列を数値に変換し、失敗理由も残す。""" numbers = [] errors = [] for item in data_list: try: numbers.append(float(item)) except ValueError: numbers.append(None) errors.append(f"{item} を変換できません") return numbers, errors
values, errors = convert_to_numbers(["10", "20.5", "abc", "30", "xyz"])print(values)print(errors)参考実装と解説
safe_calculatorは入力を数値に変換し、演算子ごとに分岐し、ZeroDivisionError、ValueError、StopIterationを分けて扱えれば合格です。サンプル入力では一度ゼロ除算の経路に入り、親切なエラーを表示してから最後のnで終了します。read_file_safelyはwith文でファイルを開き、FileNotFoundError、PermissionError、その他のOSErrorを捕捉します。失敗時にNoneを返すと、呼び出し側が次の処理を決めやすくなります。convert_to_numbersは変換できた数値と失敗理由を別々に返します。数値リストにNoneを入れることで、元のデータ位置を保ったまま不正な値も追跡できます。
| 文法 | 役割 | 使う場面 |
|---|---|---|
try | エラーが起きるかもしれないコードを囲む | エラーの可能性があるところ全般 |
except | 例外を捕捉して処理する | 対象の例外を指定して処理するとき |
else | 例外がなかったときに実行する | 成功後の処理 |
finally | 必ず実行する | リソースの片付け |
raise | 自分で例外を投げる | 入力が不正、状態が不正なとき |
| 独自例外 | 業務に合った例外を作る | 組み込み例外では説明しきれないとき |