メインコンテンツへスキップ

2.2.2 例外処理

例外処理の実行フロー図

この節の位置づけ

この節では、エラーが起きてもプログラムがすぐにクラッシュしないようにします。例外処理は、ファイルの読み書き、ネットワークリクエスト、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("この行は絶対に実行されません")

実際のプログラムでは、エラーは避けられません。ユーザーが不正なデータを入力することもありますし、ファイルが存在しないこともあります。ネットワークが切れることもあります。例外処理を使うと、こうした問題に丁寧に対応でき、プログラムを直接クラッシュさせずに済みます。


よくある例外の種類

例外の種類発生する場面
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)
ImportErrorimport に失敗するimport 存在しないモジュール

try / except の基本

try/except の流れは、まずコードを試し、エラーが起きたら代わりの処理を実行する、というものです。

try:
number = int(input("数字を入力してください: "))
print(f"入力された数字: {number}")
except ValueError:
print("入力が無効です!数字を入力してください。")

print("プログラムは続行します...") # 例外があってもなくても、この行は実行されます

実行例:

# 正常入力
数字を入力してください: 42
入力された数字: 42
プログラムは続行します...

# 数字以外を入力
数字を入力してください: abc
入力が無効です!数字を入力してください。
プログラムは続行します...

ポイントは、try/except があれば、エラーが起きてもプログラムはクラッシュしないことです。


異なる種類の例外を捕捉する

複数の例外をそれぞれ捕捉する

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)) # エラー:ゼロで割ることはできません! → None
print(safe_divide("10", 3)) # エラー:数値を渡してください! → None

複数の例外をまとめて捕捉する

try:
# エラーが起きる可能性があるコード
value = int(input("数字を入力してください: "))
result = 100 / value
print(f"結果: {result}")
except (ValueError, ZeroDivisionError) as e:
print(f"エラーが発生しました: {e}")

例外情報を取得する

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'

すべての例外を捕捉する(注意して使う)

try:
# いくつかのコード
result = risky_operation()
except Exception as e:
print(f"予期しないエラーが発生しました: {type(e).__name__}: {e}")
except Exception を乱用しない

すべての例外を捕捉すると便利そうに見えますが、本当のバグを隠してしまうことがあります。できるだけ具体的な例外の種類を捕捉し、except Exception は最終手段として外側で使いましょう。

# よくない例 ❌
try:
do_something()
except: # KeyboardInterrupt まで含めてすべて捕捉してしまう
pass # しかも何もしない!

# よい例 ✅
try:
do_something()
except ValueError:
handle_value_error()
except FileNotFoundError:
handle_file_not_found()
except Exception as e:
logging.error(f"予期しないエラー: {e}")

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 の典型的な使い方

file = None
try:
file = open("data.txt", "r")
data = file.read()
# データを処理...
except FileNotFoundError:
print("ファイルが存在しません")
finally:
if file:
file.close() # エラーの有無にかかわらず、ファイルは閉じる
print("ファイルを閉じました")
よりよい方法:with 文

後の「ファイル操作」の章で with 文を学びます。with 文はリソースの解放を自動で行ってくれるので、finally よりも簡潔です。


例外を投げる

例外を処理するだけでなく、自分で例外を投げることもできます。これは、ありえない状態や不正な状態を見つけたときに、呼び出し元へ「問題がある」と伝えるためです。

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}") # エラー: 年齢は整数でなければなりません

独自の例外を作る

組み込みの例外では足りない場合は、自分で定義できます。

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}")

実践パターン

パターン 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:再試行メカニズム

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:安全なユーザー入力

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}")

総合例:安全な成績管理システム

class GradeManager:
def __init__(self):
self.students = {}

def add_student(self, name, score):
"""学生の成績を追加する"""
if not isinstance(name, str) or not name.strip():
raise ValueError("学生名は空にできません")
if not isinstance(score, (int, float)):
raise TypeError(f"成績は数値である必要があります。受け取った型: {type(score).__name__}")
if not 0 <= score <= 100:
raise ValueError(f"成績 {score} は範囲外です(0〜100)")

self.students[name] = score
print(f"✅ 追加成功: {name} - {score} 点")

def get_average(self):
"""平均点を取得する"""
if not self.students:
raise RuntimeError("学生データがないため、平均点を計算できません")
return sum(self.students.values()) / len(self.students)

def get_student(self, name):
"""学生の成績を検索する"""
if name not in self.students:
raise KeyError(f"学生が見つかりません: {name}")
return self.students[name]

# 使用
gm = GradeManager()

# 学生を安全に追加する
test_data = [
("張三", 85),
("李四", 92),
("王五", "優秀"), # 型エラー
("趙六", 150), # 範囲エラー
("", 80), # 名前が空
("銭七", 78),
]

for name, score in test_data:
try:
gm.add_student(name, score)
except (ValueError, TypeError) as e:
print(f"❌ 追加失敗: {e}")

# 検索
print(f"\n平均点: {gm.get_average():.1f}")

try:
print(gm.get_student("孫八"))
except KeyError as e:
print(f"検索失敗: {e}")

手を動かしてみよう

練習 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:ファイル読み取り器

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:一括型変換

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)

まとめ

文法役割使う場面
tryエラーが起きるかもしれないコードを囲むエラーの可能性があるところ全般
except例外を捕捉して処理する対象の例外を指定して処理するとき
else例外がなかったときに実行する成功後の処理
finally必ず実行するリソースの片付け
raise自分で例外を投げる入力が不正、状態が不正なとき
独自例外業務に合った例外を作る組み込み例外では説明しきれないとき
コアの理解

例外処理の本質は、起こりうる問題を予測し、対処法を用意しておくことです。よいプログラムとは、エラーが起きないプログラムではなく、エラーが起きたときに丁寧に処理できるプログラムです。ユーザーにわかりやすいメッセージを出し、エラー情報を記録し、必要なら自動で再試行する。これが、初心者とプロの大きな違いです。