25. 例外をうまく処理する
第24章では、例外が発生したときにそれを読み取り、理解する方法を学びました。ここからは、例外をうまく 処理(handle) する方法を学びます。これにより、プログラムがクラッシュするのではなくエラーから回復できるようになります。これは、予期しない状況に対応できる堅牢でユーザーフレンドリーなプログラムを書くために不可欠です。
Python で例外が発生すると、プログラムの通常の流れは即座に止まります。ですが、プログラムがクラッシュする前にその例外を捕まえられたらどうでしょうか? ユーザーに再入力を促したり、デフォルト値を使ったり、問題をログに残して続行したりと、エラーに対して何らかの対応ができたらどうでしょうか? まさにそれを可能にするのが例外処理です。
25.1) try と except ブロックを使う
25.1.1) try と except の基本構造
try-except ブロック(try-except block) は、Python における「これをやってみて、例外が起きたら代わりにこちらを実行する」という方法です。基本構造は次のとおりです。
try:
# 例外を発生させる可能性のあるコード
risky_operation()
except:
# ANY の例外が発生した場合に実行されるコード
print("Something went wrong!")try ブロックには、例外を発生させる可能性のあるコードが入ります。try ブロック内のどこかで例外が発生すると、Python はただちに try ブロックの実行を止めて except ブロックへ移ります。例外が起きなければ、except ブロックは完全にスキップされます。
具体例を見てみましょう。第24章で、無効な文字列を整数に変換しようとすると ValueError が発生することを思い出してください。
# 例外処理なし - プログラムがクラッシュする
user_input = "hello"
number = int(user_input) # ValueError: invalid literal for int() with base 10: 'hello'
print("This line never executes")では、この例外をうまく処理してみましょう。
# 例外処理あり - プログラムは続行する
user_input = "hello"
try:
number = int(user_input)
print(f"You entered: {number}")
except:
print("That's not a valid number!")
number = 0 # デフォルト値を使う
print(f"Using number: {number}")Output:
That's not a valid number!
Using number: 0プログラムはクラッシュしませんでした! int(user_input) が ValueError を発生させると、Python は except ブロックへ移動し、エラーメッセージを表示し、デフォルト値を設定してから、プログラムの残りを続行しました。
ステップごとの流れは次のとおりです。
「ジャンプ」の理解 - 実際に何が起きているのか
Python が except ブロックへ「ジャンプ」すると言うとき、それは通常の逐次実行を 放棄(abandon) することを意味します。これは if 文のような単純な分岐ではなく、プログラムの流れが根本的に変わるということです。具体例で詳しく見てみましょう。
# 例外で実行の流れを観察する
print("1. Starting program")
try:
print("2. Entered try block")
number = int("hello") # 例外は HERE で発生する
print("3. After conversion") # この行は NEVER 実行されない
result = number * 2 # この行は NEVER 実行されない
print("4. After calculation") # この行は NEVER 実行されない
except ValueError:
print("5. In except block - handling the error")
print("6. After try-except block")Output:
1. Starting program
2. Entered try block
5. In except block - handling the error
6. After try-except block3行目と4行目が決して実行されないことに注目してください! int("hello") が ValueError を発生させた瞬間、Python は次のように動きます。
- 例外が起きた行で、try ブロックの実行をただちに 停止 します
- この種類の例外を処理できる、対応する except 節を 探索 します
- その except ブロックへ ジャンプ し、try ブロック内の残りのコードをすべてスキップします
- except ブロックが終わると、try-except 構造の後で 続行 します
これは通常のプログラムの流れとは本質的に異なります。通常の実行では、Python は各行を順番に実行します。例外があると、Python は現在の経路を放棄し、まったく別のルートを取ります。例外処理がなければ、プログラムは2行目でクラッシュして終了します。例外処理があれば、プログラムは回復して続行できます。
これが重要な理由:
この「ジャンプ」動作を理解することが重要なのは、次の理由からです。
- try ブロック内で例外より後のコードはスキップされます - try ブロックの後半が実行されたと想定できません
- 例外が代入より前に起きた場合、変数が初期化されない 可能性があります
- except ブロックが動くときに プログラムがどんな状態か を考えておく必要があります
25.1.2) ユーザー入力を安全に扱う
例外処理の最も一般的な使い道の1つは、ユーザー入力の検証です。ユーザーは何でも入力できるため、不正な入力をうまく扱う必要があります。ここでは、ユーザーの年齢を尋ねるプログラムの実用的な例を示します。
# 例外処理を使った安全な年齢入力
print("Please enter your age:")
user_input = input()
try:
age = int(user_input)
print(f"You are {age} years old.")
# 生年を計算する(現在の年が 2024 年だと仮定)
birth_year = 2024 - age
print(f"You were born around {birth_year}.")
except:
print("Invalid input! Age must be a number.")
print("Using default age of 0.")
age = 0ユーザーが "25" と入力した場合、出力は次のとおりです。
Please enter your age:
25
You are 25 years old.
You were born around 1999.ユーザーが "twenty-five" と入力した場合、出力は次のとおりです。
Please enter your age:
twenty-five
Invalid input! Age must be a number.
Using default age of 0.トレースバックを出してクラッシュするのではなく、プログラムがうまくエラーを処理していることに注目してください。このほうがユーザー体験としてはるかに良くなります。
25.1.3) try ブロックで複数の操作を扱う
1つの try ブロックに複数の操作を入れられます。それらのどれかが例外を発生させると、Python はただちに except ブロックへジャンプします。まずは簡単な例から始めましょう。
# try ブロックに 2 つの操作
print("Enter a number:")
user_input = input()
try:
number = int(user_input) # 1つ目の操作 - ValueError を発生させる可能性がある
result = 100 / number # 2つ目の操作 - ZeroDivisionError を発生させる可能性がある
print(f"100 / {number} = {result}")
except:
print("Something went wrong!")ユーザーが "hello" と入力すると、1つ目の操作(変換)で例外が発生します。ユーザーが "0" と入力すると、2つ目の操作(除算)で例外が発生します。どちらの場合でも、単一の except ブロックが捕捉します。
次に、これを 3 つの操作に拡張してみましょう。
# try ブロックに複数の操作
print("Enter two numbers to divide:")
numerator_input = input("Numerator: ")
denominator_input = input("Denominator: ")
try:
numerator = int(numerator_input) # ValueError を発生させる可能性がある
denominator = int(denominator_input) # ValueError を発生させる可能性がある
result = numerator / denominator # ZeroDivisionError を発生させる可能性がある
print(f"{numerator} / {denominator} = {result}")
except:
print("Something went wrong with the calculation!")
print("Make sure you enter valid numbers and don't divide by zero.")ユーザーが "10" と "2" を入力した場合:
Enter two numbers to divide:
Numerator: 10
Denominator: 2
10 / 2 = 5.0ユーザーが "10" と "zero" を入力した場合:
Enter two numbers to divide:
Numerator: 10
Denominator: zero
Something went wrong with the calculation!
Make sure you enter valid numbers and don't divide by zero.ユーザーが "10" と "0" を入力した場合:
Enter two numbers to divide:
Numerator: 10
Denominator: 0
Something went wrong with the calculation!
Make sure you enter valid numbers and don't divide by zero.この例では、3つの異なる問題が起こりえます。分子の変換が失敗する、分母の変換が失敗する、または分母が0の場合に割り算が失敗する、です。単一の except ブロックがこれらすべてのケースを捕捉します。とはいえ、この方法には制限があります。どの具体的なエラーが発生したのかを判別できません。これは次のセクションで扱います。
25.1.4) bare except 節の問題
例外型を指定せずに except: を使うことを bare except 節(bare except clause) と呼びます。これはすべての例外を捕捉しますが、範囲が広すぎて予期しない問題を隠してしまうことがあります。次の例を考えてみてください。
# bare except は EVERYTHING を捕捉する - 想定していないものまで
numbers = [10, 20, 30]
try:
index = 5 # index が範囲外なら IndexError を想定する
value = numbers[index]
print(f"Value at index {index}: {value}")
except:
print("Could not access the list element.")これは妥当に見えます。存在しないかもしれないリスト要素へアクセスしています。しかし、コードにタイプミスがあったらどうなるでしょうか?
# コードにタイプミスがあったら?
numbers = [10, 20, 30]
try:
index = 2
value = numbrs[index] # Typo: 'numbers' の代わりに 'numbrs'
print(f"Value at index {index}: {value}")
except:
print("Could not access the list element.")Output:
Could not access the list element.bare except がタイプミスによる NameError を捕捉してしまい、「Could not access the list element.」と表示します。これでは何が問題なのかについて誤った情報が提示されます! インデックスが範囲外だと思ってしまいますが、実際には変数名にタイプミスがあります。
bare except は KeyboardInterrupt(ユーザーが Ctrl+C を押したとき)や SystemExit(プログラムが終了しようとしたとき)も捕捉してしまいますが、通常これらは捕捉すべきではありません。こうした理由から、次に学ぶ「特定の例外を捕捉する」ほうがよいのです。
25.2) 特定の例外を捕捉する
25.2.1) 例外型を指定する
bare except ですべての例外を捕捉する代わりに、処理したい例外型を指定できます。これにより、コードがより正確になり、異なるエラーに対して適切に反応できるようになります。
# 特定の例外型を捕捉する
user_input = "hello"
try:
number = int(user_input)
print(f"You entered: {number}")
except ValueError:
print("That's not a valid number!")
number = 0
print(f"Using number: {number}")Output:
That's not a valid number!
Using number: 0これで except 節は ValueError 例外だけを捕捉します。別の種類の例外(タイプミスによる NameError など)が起きた場合は捕捉されず、完全なトレースバックが表示されます。これはデバッグにとってむしろ役立ちます。
構文は except ExceptionType: です。ここで ExceptionType は捕捉したい例外クラス名(ValueError、TypeError、ZeroDivisionError など)です。
よくある間違い: 間違った例外型を捕捉する
実際に発生した例外と一致しない例外型を指定するとどうなるでしょうか? 見てみましょう。
# 間違った例外型を捕捉する
user_input = "hello"
try:
number = int(user_input) # これは ValueError を発生させる
print(f"You entered: {number}")
except TypeError: # しかし TypeError を捕捉している!
print("That's not a valid number!")
number = 0
print(f"Using number: {number}")Output:
Traceback (most recent call last):
File "example.py", line 4, in <module>
number = int(user_input)
ValueError: invalid literal for int() with base 10: 'hello'プログラムはクラッシュしました! なぜでしょうか? int("hello") は ValueError を発生させますが、except 節は TypeError しか捕捉しないからです。対応する except 節がないため例外は捕捉されず、プログラムは終了します。
これは開発中にはむしろ役に立ちます。間違った例外型を捕捉していると、完全なトレースバックが表示され、ミスに気づけるからです。これは bare except より特定の例外を捕捉するほうが良い理由の1つでもあります。
この間違いを避ける方法:
- トレースバックを読んで、実際に発生した例外型を確認します
- その具体的な例外型を except 節で使います
- 自信がなければ、コードを実行してクラッシュさせます。トレースバックが教えてくれます!
25.2.2) 異なる例外を異なる方法で扱う
異なる例外型を異なる方法で処理するために、複数の except 節を持てます。これは、異なるエラーに対して異なる対応が必要なときに非常に便利です。
# 例外ごとに異なる扱いをする
print("Enter two numbers to divide:")
numerator_input = input("Numerator: ")
denominator_input = input("Denominator: ")
try:
numerator = int(numerator_input)
denominator = int(denominator_input)
result = numerator / denominator
print(f"{numerator} / {denominator} = {result}")
except ValueError:
print("Error: Both inputs must be valid integers.")
print("You entered something that isn't a number.")
except ZeroDivisionError:
print("Error: Cannot divide by zero.")
print("The denominator must be a non-zero number.")ユーザーが "10" と "abc" を入力した場合:
Enter two numbers to divide:
Numerator: 10
Denominator: abc
Error: Both inputs must be valid integers.
You entered something that isn't a number.ユーザーが "10" と "0" を入力した場合:
Enter two numbers to divide:
Numerator: 10
Denominator: 0
Error: Cannot divide by zero.
The denominator must be a non-zero number.Python は except 節を順にチェックします。例外が発生すると、その例外型に一致する最初の except 節を見つけて、そのブロックを実行します。他の except 節はスキップされます。
25.2.3) 1つの節で複数の例外型を捕捉する
複数の例外型を同じように処理したい場合があります。複数の同一な except ブロックを書く代わりに、例外型をタプルとして括弧内に並べることで、1つの節で複数の例外型を捕捉できます。
# 複数の例外型をまとめて捕捉する
print("Enter a number:")
user_input = input()
try:
number = int(user_input)
result = 100 / number
print(f"100 divided by {number} is {result}")
except (ValueError, ZeroDivisionError):
print("Invalid input or division by zero.")
print("Please enter a non-zero number.")ユーザーが "hello" を入力した場合:
Enter a number:
hello
Invalid input or division by zero.
Please enter a non-zero number.ユーザーが "0" を入力した場合:
Enter a number:
0
Invalid input or division by zero.
Please enter a non-zero number.(不正な変換による)ValueError と(ゼロ除算による)ZeroDivisionError の両方が、同じ except 節で処理されています。異なるエラーでも同じ対応をしたい場合に便利です。
25.2.4) 例外情報にアクセスする
発生した例外について、より詳細を知りたい場合があります。as キーワードを使うと、例外オブジェクトを捕捉できます。ですがその前に、例外オブジェクトが実際に何なのかを理解しましょう。
例外オブジェクトとは?
Python が例外を発生させるとき、単に「何かが間違っている」と通知するだけではなく、そのエラーに関する情報を含む オブジェクト(object) を作ります。この例外オブジェクトは、次のような情報を含む詳細なエラーレポートのようなものです。
- エラーメッセージ: 何が問題だったかの説明
- 例外型: どの種類のエラーが起きたか(ValueError、TypeError など)
- 追加属性: 例外型に応じた固有の情報
例外オブジェクトは、エラーに関するすべての情報を保持するコンテナだと考えてください。リストオブジェクトが要素を含み append() のようなメソッドを持つのと同様に、例外オブジェクトもエラー情報を含み、アクセスできる属性を持ちます。
except ValueError as error: と書くと、Python に対して「ValueError が起きたら error という変数を作り、そこに例外オブジェクトを入れて調べられるようにして」という意味になります。
例外オブジェクトの中身を見てみましょう。
# 例外オブジェクトの内容を調べる
try:
number = int("hello")
except ValueError as error:
print("Exception caught! Let's examine it:")
print(f"Type: {type(error)}")
print(f"String representation: {error}")
print(f"Args tuple: {error.args}")Output:
Exception caught! Let's examine it:
Type: <class 'ValueError'>
String representation: invalid literal for int() with base 10: 'hello'
Args tuple: ("invalid literal for int() with base 10: 'hello'",)例外オブジェクトには次のものがあります。
- type(ValueError クラス)- どんな種類のエラーが起きたかを示します
- 文字列表現(エラーメッセージ)- トレースバックで見る内容です
- args 属性(メッセージやその他の引数を含むタプル)- エラー詳細へ構造化された形でアクセスできます
これが重要な理由:
例外型ごとに、固有の情報を提供する異なる属性があります。例外オブジェクトの構造を理解すると、デバッグやユーザーフィードバックに役立つ情報を取り出せるようになります。
# 例外によって異なる属性を持つ
numbers = [10, 20, 30]
try:
value = numbers[10]
except IndexError as error:
print(f"IndexError message: {error}")
print(f"Exception args: {error.args}")
# 次は辞書で試す
grades = {"Alice": 95}
try:
grade = grades["Bob"]
except KeyError as error:
print(f"KeyError message: {error}")
print(f"Missing key: {error.args[0]}")Output:
IndexError message: list index out of range
Exception args: ('list index out of range',)
KeyError message: 'Bob'
Missing key: BobKeyError が、欠けていた実際のキーをメッセージに含むことに注目してください。例外型によって、例外オブジェクトを通じてアクセスできる有用な情報が異なります。
25.3) try ブロックで else と finally を使う
25.3.1) else 節: 成功したときだけ実行されるコード
try-except ブロックの else 節は、try ブロックで 例外が発生しなかった場合のみ 実行されます。これは、危険な操作が成功したときだけ実行したいコードに便利です。
# 成功時のみ実行するコードに else を使う
print("Enter a number:")
user_input = input()
try:
number = int(user_input)
except ValueError:
print("That's not a valid number!")
else:
# int(user_input) が成功した場合のみ実行される
print(f"Successfully converted: {number}")
squared = number ** 2
print(f"The square of {number} is {squared}")ユーザーが "5" を入力した場合:
Enter a number:
5
Successfully converted: 5
The square of 5 is 25ユーザーが "hello" を入力した場合:
Enter a number:
hello
That's not a valid number!なぜ try ブロックの末尾にコードを書くだけではなく else を使うのでしょうか? 重要な理由が2つあります。
- 明確さ:
else節により、このコードが成功時のみ実行されることが明示されます - 例外スコープ:
else節で発生した例外は、直前のexcept節では捕捉されません
2つ目が重要である理由を示す例です。
# else が例外スコープに役立つ理由を示す
try:
number_1 = int(input("Enter a number_1: "))
except ValueError:
print("Invalid input!")
else:
# ここでエラーが発生しても、上の except では捕捉されない
# これにより入力エラーと処理エラーを区別しやすくなる
number_2 = int(input("Enter a number_2: ")) # ValueError を発生させる可能性があるnumber_2 = int(input(...)) を number_1 と一緒に try ブロックへ入れてしまうと、どちらの入力からの ValueError も同じ except ValueError 節で捕捉されます。これでは、どちらの入力が問題を起こしたのか判別できません。
number_2 = int(input(...)) を else ブロックに入れることで、エラー処理を分離できます。except 節は number_1 のエラーだけを捕捉し、number_2 のエラーは捕捉されずに完全なトレースバックが出ます。これにより、最初ではなく2つ目の入力が失敗したことが明確になります。
25.3.2) finally 節: 常に実行されるコード
finally 節には 何が起きても 実行されるコードが入ります。例外が発生したかどうか、捕捉されたかどうかにかかわらず実行されます。これは、常に実行しなければならない後始末(クリーンアップ)処理に不可欠です。
# 後始末に finally を使う
print("Enter a number:")
user_input = input()
try:
number = int(user_input)
result = 100 / number
print(f"Result: {result}")
except ValueError:
print("Invalid number!")
except ZeroDivisionError:
print("Cannot divide by zero!")
finally:
print("Calculation attempt completed.")ユーザーが "5" を入力した場合:
Enter a number:
5
Result: 20.0
Calculation attempt completed.ユーザーが "hello" を入力した場合:
Enter a number:
hello
Invalid number!
Calculation attempt completed.ユーザーが "0" を入力した場合:
Enter a number:
0
Cannot divide by zero!
Calculation attempt completed.3つすべてのケースで finally ブロックが実行されます! これが finally の重要な挙動です。try ブロックで何が起きても 必ず 実行されます。
25.3.3) try、except、else、finally を組み合わせる
4つすべての節を組み合わせることで、包括的な例外処理を作れます。
# 完全な例外処理構造
print("Enter a number to calculate its reciprocal:")
user_input = input()
try:
# 危険な操作
number = int(user_input)
reciprocal = 1 / number
except ValueError:
# 変換エラーを処理する
print("Error: Input must be a valid integer.")
except ZeroDivisionError:
# ゼロ除算を処理する
print("Error: Cannot calculate reciprocal of zero.")
else:
# 成功時のみのコード
print(f"The reciprocal of {number} is {reciprocal}")
print(f"Verification: {number} × {reciprocal} = {number * reciprocal}")
finally:
# 常に実行される後始末コード
print("Reciprocal calculation completed.")ユーザーが "4" を入力した場合:
Enter a number to calculate its reciprocal:
4
The reciprocal of 4 is 0.25
Verification: 4 × 0.25 = 1.0
Reciprocal calculation completed.ユーザーが "hello" を入力した場合:
Enter a number to calculate its reciprocal:
hello
Error: Input must be a valid integer.
Reciprocal calculation completed.ユーザーが "0" を入力した場合:
Enter a number to calculate its reciprocal:
0
Error: Cannot calculate reciprocal of zero.
Reciprocal calculation completed.実行の流れは次のとおりです。
tryブロックが常に最初に実行されます- 例外が発生したら、一致する
exceptブロックが実行されます - 例外が発生しなければ、(あれば)
elseブロックが実行されます finallyブロックは、何が起きても最後に必ず実行されます
25.4) raise で意図的に例外を発生させる
25.4.1) なぜ例外を raise するのか?
ここまでは、Python が自動的に発生させる例外を捕捉してきました。しかし、ときにはあなた自身のコードで、意図的に例外を発生させる必要があります。これは次のような場合に役立ちます。
- コードが対処できない不正な状況を検出したとき
- ルールや制約を強制したいとき
- 関数を呼び出したコードにエラーを通知したいとき
例外を発生させることは、Python における「これ以上続けられない。何かが間違っていて、呼び出し元が対処する必要がある」という伝え方です。
構文は簡単で、raise ExceptionType("error message") です。
基本例を示します。
# 意図的に例外を発生させる
age = -5
if age < 0:
raise ValueError("Age cannot be negative!")
print(f"Age: {age}") # この行は実行されないOutput:
Traceback (most recent call last):
File "example.py", line 5, in <module>
raise ValueError("Age cannot be negative!")
ValueError: Age cannot be negative!Python が raise に遭遇すると、ただちに例外を作り、それを処理する except ブロックを探し始めます。見つからなければ、プログラムはトレースバック付きで終了します。
25.4.2) 関数で例外を発生させる
例外を発生させることは、関数(function) で入力を検証し、制約を強制するのに特に便利です。
# 例外を発生させて入力を検証する関数
def calculate_discount(price, discount_percent):
"""割引後価格を計算する。
Args:
price: 元の価格(正の値である必要がある)
discount_percent: 割引率(0-100 である必要がある)
Returns:
割引後価格
Raises:
ValueError: 入力が不正な場合
"""
if price < 0:
raise ValueError("Price cannot be negative!")
if discount_percent < 0 or discount_percent > 100:
raise ValueError("Discount must be between 0 and 100!")
discount_amount = price * (discount_percent / 100)
return price - discount_amount
# 関数を使う
try:
final_price = calculate_discount(100, 20)
print(f"Final price: ${final_price}")
except ValueError as error:
print(f"Error: {error}")Output:
Final price: $80.0では、不正な入力で試してみましょう。
# 不正な価格
try:
final_price = calculate_discount(-50, 20)
print(f"Final price: ${final_price}")
except ValueError as error:
print(f"Error: {error}")Output:
Error: Price cannot be negative!# 不正な割引率
try:
final_price = calculate_discount(100, 150)
print(f"Final price: ${final_price}")
except ValueError as error:
print(f"Error: {error}")Output:
Error: Discount must be between 0 and 100!例外を発生させることで、この関数は何が問題だったのかを明確に伝えます。呼び出し側のコードは、エラーをどう扱うかを決められます。たとえば、ユーザーに再入力させる、デフォルト値を使う、エラーをログに残す、といった対応です。
25.4.3) 適切な例外型を選ぶ
Python には多くの組み込み例外型があり、適切なものを選ぶとコードがより明確になります。検証に最もよく使われる例外は次のとおりです。
- ValueError: 型は正しいが値が不適切な場合に使います(例: 負の年齢、無効な割合)
- TypeError: 値の型が完全に間違っている場合に使います(例: 数値の代わりに文字列)
- KeyError: 辞書のキーが存在しない場合に使います
- IndexError: シーケンスのインデックスが範囲外の場合に使います
異なる例外型を示す例です。
# 適切な例外型を使う
def get_student_grade(grades, student_name):
"""grades 辞書から生徒の成績を取得する。
Args:
grades: 生徒名から成績への対応を持つ辞書
student_name: 生徒名
Returns:
生徒の成績
Raises:
TypeError: grades が辞書でない場合
KeyError: student_name が grades に存在しない場合
ValueError: 成績が不正な場合
"""
if not isinstance(grades, dict):
raise TypeError("Grades must be a dictionary!")
if student_name not in grades:
raise KeyError(f"Student '{student_name}' not found!")
grade = grades[student_name]
if not (0 <= grade <= 100):
raise ValueError(f"Invalid grade: {grade} (must be 0-100)")
return grade
# 正しいデータでテスト
grades = {"Alice": 95, "Bob": 87, "Carol": 92}
try:
grade = get_student_grade(grades, "Alice")
print(f"Alice's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
print(f"Error: {error}")Output:
Alice's grade: 95# 存在しない生徒でテスト
try:
grade = get_student_grade(grades, "David")
print(f"David's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
print(f"Error: {error}")Output:
Error: Student 'David' not found!# 型が間違っている場合のテスト
try:
grade = get_student_grade("not a dict", "Alice")
print(f"Alice's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
print(f"Error: {error}")Output:
Error: Grades must be a dictionary!適切な例外型を使うと、他のプログラマー(そして将来の自分)が、どんな種類のエラーが起きたのかを理解しやすくなります。
25.4.4) 例外の再送出
例外を捕捉し、何か(ログ出力など)を行ってから、例外を伝播させ続けたい場合があります。これを行うには、except ブロック内で引数なしの raise を使います。
# ログ出力の後に例外を再送出する
def divide_numbers(a, b):
"""2つの数を割り算し、エラーログを出す。"""
try:
result = a / b
return result
except ZeroDivisionError:
print("ERROR LOG: Division by zero attempted")
print(f" Numerator: {a}, Denominator: {b}")
raise # 同じ例外を再送出する
# 関数を使う
try:
result = divide_numbers(10, 0)
print(f"Result: {result}")
except ZeroDivisionError:
print("Cannot divide by zero!")Output:
ERROR LOG: Division by zero attempted
Numerator: 10, Denominator: 0
Cannot divide by zero!引数なしの raise は、たった今捕捉した例外を再送出します。これは次のようなときに役立ちます。
- エラーをログに残す/記録する
- 後始末をする
- エラーを呼び出し元へ伝播させる
25.4.5) 例外から例外を発生させる
別の例外を処理している最中に、新しい例外を発生させたい場合があります。その際、元のエラーの文脈も保持したいことがあります。Python 3 はこのために raise ... from ... 構文を提供しています。
# 既存の例外から新しい例外を発生させる
def load_config(config_dict, key):
"""辞書から設定値を読み込む。"""
try:
config_value = config_dict[key]
# 整数としてパースを試みる
parsed_value = int(config_value)
return parsed_value
except KeyError as error:
raise RuntimeError(f"Configuration key missing: {key}") from error
except ValueError as error:
raise RuntimeError(f"Invalid configuration format for {key}") from error
# 関数を使う
config = {"timeout": "30", "retries": "5"}
try:
value = load_config(config, "timeout")
print(f"Config value: {value}")
except RuntimeError as error:
print(f"Configuration error: {error}")
print(f"Original cause: {error.__cause__}")Output:
Config value: 30キーが存在しない場合:
try:
value = load_config(config, "missing_key")
print(f"Config value: {value}")
except RuntimeError as error:
print(f"Configuration error: {error}")
print(f"Original cause: {error.__cause__}")Output:
Configuration error: Configuration key missing: missing_key
Original cause: 'missing_key'from キーワードは、新しい例外を元の例外へ関連付けます。これにより例外のチェーンが作られ、デバッグに役立ちます。高レベルで何が問題だったか(設定エラー)と、根本原因が何だったか(キーが見つからない)を両方確認できます。
例外処理は、信頼性の高いプログラムを書くための最も重要な道具の1つです。try-except ブロックを使えば、問題を予測し、うまく処理し、ユーザーにより良い体験を提供できます。次の点を覚えておきましょう。
- 予測されるエラーをうまく処理するために
try-exceptを使う - bare
exceptを使うのではなく、特定の例外型を捕捉する - 成功時のみ実行したいコードには
elseを使う - 必ず実行しなければならない後始末コードには
finallyを使う - 問題を通知するために、自分のコードで例外を発生させる
- エラーを明確にするために、適切な例外型を選ぶ
- 何が問題だったかを説明する、役に立つエラーメッセージを用意する
次の章では、例外処理と入力検証、その他の戦略を組み合わせて、プログラムをさらに堅牢にする防御的プログラミング(defensive programming)の技法を学びます。