24. エラーとトレースバックの理解
エラーはプログラミングにおいて避けられないものです。初心者から熟練者まで、すべてのプログラマーが定期的に遭遇します。エラーに苦しむか、そこから学ぶかの違いは、何かがうまくいかないときに Python があなたに何を伝えようとしているのかを理解できるかどうかにあります。
Python がコード内の問題に遭遇すると、ただ黙って止まるのではなく、何が起きたのか、どこで起きたのか、そして多くの場合はなぜ起きたのかのヒントまで、詳細な情報を提供します。これらのエラーメッセージを読み、解釈できるようになることは、プログラマーとして身につけられる最も価値のあるスキルの 1 つです。
この章では、遭遇するエラーの主な 2 つのカテゴリ、構文エラー(syntax errors)(コードの書き方に関する問題)と 実行時例外(runtime exceptions)(コード実行中に起きる問題)を取り上げます。Python の詳細なエラーレポートである トレースバック(tracebacks) を読み、例外がプログラムの通常の流れをどのように変えるのかを理解します。そして最も重要なのは、エラーを失敗ではなく、より良いコードを書くために役立つ価値ある情報として扱うデバッグ思考を身につけることです。
24.1) 構文エラー vs 実行時例外
Python はコード内の問題を、根本的に異なる 2 つのタイプに区別します。構文エラーと実行時例外です。この違いを理解することで、問題をより素早く診断し、解決策を探すべき場所が分かるようになります。
24.1.1) 構文エラーとは
構文エラー(syntax error) は、言語の文法規則に違反しているために Python がコードを理解できないときに発生します。「The cat sat on the」が不完全な英語の文であるのと同じように、構文エラーを含むコードは不完全または不正な Python であり、インタープリタ(interpreter)が解析(parse)できません。
構文エラーは、プログラムが実行される前に検出されます。Python はまずスクリプト全体を読み、言語のルールに従っているかを確認します。構文エラーを見つけると、正しい部分があったとしても、コードを一切実行しません。
簡単な例を示します。
# 警告: 構文エラー - デモ用のみ
# ミス: if 文の後にコロンがない
age = 25
if age >= 18
print("You are an adult")このコードを実行しようとすると、Python はすぐに次のように報告します。
File "example.py", line 3
if age >= 18
^
SyntaxError: expected ':'このエラーメッセージには、いくつかの重要な特徴があります。
- ファイル名と行番号: Python は問題を見つけた場所を正確に教えてくれます(
line 3) - 視覚的な指標: キャレット(
^)が、Python が混乱した位置を指します - エラー種別:
SyntaxErrorが、文法の問題であることを明確に示しています - 役立つヒント:
expected ':'が、何が足りないかを教えてくれます
構文が無効なため、コードは決して実行されません。Python は実行を開始することすらできないのです。
別のよくある構文エラーも見てみましょう。
# 警告: 構文エラー - デモ用のみ
# ミス: 括弧の対応が取れていない
numbers = [1, 2, 3, 4, 5]
total = sum(numbers
print(f"Total: {total}")Python は次のように報告します。
File "example.py", line 2
total = sum(numbers
^
SyntaxError: '(' was never closedここでは、2 行目で括弧を開いたものの、閉じていないことを Python が検出しました。エラーは 2 行目(閉じられていない括弧がある行)として報告され、キャレットが閉じ括弧を期待していた位置を指しています。
構文エラーの主な特徴:
- どのコードも実行される前に検出される
- プログラム全体の実行を妨げる
- たいていはタイプミス、句読点の抜け、インデントの誤りを示す
- エラー位置が、実際のミスより少し後ろになることがある
24.1.2) 実行時例外とは
実行時例外(runtime exception)(または単に「例外(exception)」)は、構文的には正しいコードが実行中に問題に遭遇したときに発生します。コードは文法的に正しい Python ですが、プログラムが実際に動くときに何かがうまくいきません。
構文エラーとは異なり、例外はプログラムの実行中に起きます。Python はコードを問題なく解析して実行を開始したものの、対処できない状況に遭遇します。
簡単な例を示します。
# このコードは構文的には正しいが、例外を送出します
numbers = [10, 20, 30]
print(numbers[0]) # Output: 10
print(numbers[5]) # この行で IndexError が発生します
print("This line never executes")Output:
10
Traceback (most recent call last):
File "example.py", line 3, in <module>
print(numbers[5])
~~~~~~~^^^
IndexError: list index out of range起きたことを確認しましょう。
- 1 つ目の
print文は正常に実行されました(10が表示されました) - 2 つ目の
printは、存在しない index 5 にアクセスしようとしました - Python は
IndexError例外を送出しました - プログラムは停止し、3 つ目の
printは実行されませんでした
コードは構文的に正しく、Python は何をしたいのか理解できていました。問題は、存在しないリスト要素にアクセスしようとした実行中に発生しました。
別の種類の実行時例外の例も見てみましょう。
# 構文は正しいが、実行時に 0 で割っています
def calculate_average(total, count):
return total / count
# これらは問題なく動きます
print(calculate_average(100, 4)) # Output: 25.0
print(calculate_average(75, 3)) # Output: 25.0
# これは例外を送出します
print(calculate_average(50, 0)) # ZeroDivisionErrorOutput:
25.0
25.0
Traceback (most recent call last):
File "example.py", line 8, in <module>
print(calculate_average(50, 0))
^^^^^^^^^^^^^^^^^^^^^^^^
File "example.py", line 2, in calculate_average
return total / count
~~~~~~^~~~~~~
ZeroDivisionError: division by zero関数は 2 回は完璧に動きましたが、3 回目の呼び出しでは count に 0 を渡したため 0 除算が発生しました。Python はその具体的な値でコードが実行されるまで、この問題を検出できません。
実行時例外の主な特徴:
- プログラム実行中に発生する
- コードは構文的に正しい
- 特定のデータや条件に依存することが多い
- 例外が起きた地点までプログラムは実行される
- 入力が違えば例外の種類が変わることもあれば、まったく発生しないこともある
24.1.3) 構文エラーと実行時例外の比較
違いを理解するために、2 種類のエラーを並べて見てみましょう。
# 例 1: 構文エラー
# ミス: 閉じクォートがない
print("Program started!")
message = "Hello, world
print(message)これはすぐに構文エラーになります。
File "example.py", line 4
message = "Hello, world
^
SyntaxError: unterminated string literal (detected at line 4)重要: 出力に "Program started!" が表示されていないことに注目してください。Python はどのコードも実行する前に構文エラーを検出しました。
次に実行時例外と比較します。
# 例 2: 実行時例外
# 構文は正しいが、変数が存在しない
print("Program started!")
message = "Hello, world"
print(mesage) # Typo: 'mesage' instead of 'message'Output:
Program started!
Traceback (most recent call last):
File "example.py", line 5, in <module>
print(mesage)
^^^^^^
NameError: name 'mesage' is not defined重要: 今回は出力に "Program started!" が表示されています。Python は 1 つ目と 2 つ目の print と代入文(3〜4 行目)を正常に実行しましたが、5 行目で mesage を見つけようとしたときに問題に遭遇しました。
決定的な違い: 1 つ目の例では、Python はコードを実行しようとすらせず、解析中に構文エラーを見つけました。2 つ目の例では、Python はプログラムの実行を開始し、複数行を実行した後に実行時エラーに遭遇しました。
24.2) よくある組み込み例外の種類
Python には多くの組み込み例外があり、それぞれが特定の種類の問題を表しています。これらのよくある例外を認識できるようになると、何が起きたのか、どう直せばよいのかを素早く理解できます。各例外タイプには、問題を示唆する説明的な名前が付いています。
24.2.1) NameError: 未定義の名前を使う
NameError は、Python が認識しない変数・関数・その他の名前を使おうとしたときに発生します。たいていは、何かを定義し忘れた、名前を打ち間違えた、または作成される前に使おうとしたことが原因です。
# 例 1: 変数を定義し忘れた
print(greeting) # NameError: name 'greeting' is not definedOutput:
Traceback (most recent call last):
File "example.py", line 2, in <module>
print(greeting)
^^^^^^^^
NameError: name 'greeting' is not definedPython は greeting が何なのか分からないと言っています。先に作る必要があります。
# 正しいバージョン
greeting = "Hello, Python!"
print(greeting) # Output: Hello, Python!タイプミスの、もう少し微妙な例です。
# 例 2: 変数名のタイプミス
user_name = "Alice"
age = 30
print(f"{username} is {age} years old") # NameError: name 'username' is not defineduser_name(アンダースコアあり)を定義したのに、username(アンダースコアなし)を使おうとしました。Python はこれらを完全に別の名前として扱います。
24.2.2) TypeError: 操作に対する型が間違っている
TypeError は、間違った型の値に対して操作を行おうとしたときに発生します。たとえば、文字列に整数を足すことはできませんし、関数ではないものを呼び出すこともできません。
# 例 1: 互換性のない型を混ぜる
age = 25
message = "You are " + age + " years old" # TypeErrorOutput:
Traceback (most recent call last):
File "example.py", line 2, in <module>
message = "You are " + age + " years old"
~~~~~~~~~~~~^~~~~
TypeError: can only concatenate str (not "int") to strPython は、+ 演算子で文字列同士は連結できるが、文字列と整数は連結できないと言っています。整数を文字列に変換する必要があります。
# 正しいバージョン
age = 25
message = "You are " + str(age) + " years old"
print(message) # Output: You are 25 years oldTypeError は、関数に渡す引数の数が間違っている場合にも発生します。
# 例 3: 引数の数が間違っている
def calculate_area(length, width):
return length * width
area = calculate_area(5) # TypeError: missing 1 required positional argumentOutput:
Traceback (most recent call last):
File "example.py", line 4, in <module>
area = calculate_area(5)
TypeError: calculate_area() missing 1 required positional argument: 'width'この関数は引数を 2 つ期待していますが、1 つしか渡していません。
24.2.3) ValueError: 型は正しいが値が不適切
ValueError は、正しい型の値を渡したものの、その値自体が操作に対して不適切なときに発生します。型は合っていますが、その文脈では具体的な値が意味を成しません。
# 例 1: 無効な文字列を整数に変換する
user_input = "twenty-five"
age = int(user_input) # ValueError: invalid literal for int()Output:
Traceback (most recent call last):
File "example.py", line 2, in <module>
age = int(user_input)
ValueError: invalid literal for int() with base 10: 'twenty-five'int() 関数は文字列を期待しており、私たちは文字列を渡しています。つまり型は正しいです。しかし、"twenty-five" は文字を含むため整数に変換できません。文字列 "25" なら問題なく動きます。
# 正しいバージョン
user_input = "25"
age = int(user_input)
print(age) # Output: 25ValueError はリストのメソッドでも発生します。
# 例 3: 存在しない要素を削除する
fruits = ["apple", "banana", "orange"]
fruits.remove("grape") # ValueError: 'grape' is not in listOutput:
Traceback (most recent call last):
File "example.py", line 2, in <module>
fruits.remove("grape")
~~~~~~~~~~~~~^^^^^^^^^
ValueError: list.remove(x): x not in listremove() メソッドは、リスト内に存在する値を期待します。先に確認すべきです。
# 正しいバージョン
fruits = ["apple", "banana", "orange"]
if "grape" in fruits:
fruits.remove("grape")
else:
print("Grape not found in list") # Output: Grape not found in list24.2.4) IndexError: 無効なシーケンスのインデックス
IndexError は、存在しない index を使ってシーケンス(リスト、タプル、文字列)にアクセスしようとしたときに発生します。Python は 0 始まりのインデックスを使い、有効な index は 0 から len(sequence) - 1 までです。
# 例 1: インデックスが大きすぎる
colors = ["red", "green", "blue"]
print(colors[0]) # Output: red
print(colors[3]) # IndexError: list index out of rangeOutput:
red
Traceback (most recent call last):
File "example.py", line 3, in <module>
print(colors[3])
~~~~~~^^^
IndexError: list index out of rangeこのリストには index 0、1、2 に 3 つの要素があります。index 3 は存在しません。これは、インデックスが 0 から始まることを忘れたときに非常によく起きるミスです。
# 正しいバージョン
colors = ["red", "green", "blue"]
print(colors[2]) # Output: blue (the third element)24.2.5) KeyError: 辞書のキーが存在しない
KeyError は、存在しない辞書のキーにアクセスしようとしたときに発生します。リストのように長さを見て判断できるわけではなく、辞書には任意のキーが入り得るため、アクセスする前にキーが存在することを確認する必要があります。
# 例 1: 存在しないキーにアクセスする
student = {
"name": "Alice",
"age": 20,
"major": "Computer Science"
}
print(student["name"]) # Output: Alice
print(student["grade"]) # KeyError: 'grade'Output:
Alice
Traceback (most recent call last):
File "example.py", line 7, in <module>
print(student["grade"])
~~~~~~~^^^^^^^^^
KeyError: 'grade'この辞書には "grade" キーがありません。先にキーが存在するかを確認できます。
# 'in' を使った正しいバージョン
student = {
"name": "Alice",
"age": 20,
"major": "Computer Science"
}
if "grade" in student:
print(student["grade"])
else:
print("Grade not available") # Output: Grade not availableまたは、エラーを送出する代わりに None(またはデフォルト値)を返す get() メソッドを使います。
# get() を使った別案
grade = student.get("grade")
if grade is not None:
print(f"Grade: {grade}")
else:
print("Grade not available") # Output: Grade not availableKeyError は、構造が一貫しないデータを処理しているときによく起きます。
# 例 2: 複数レコードの処理
students = [
{"name": "Alice", "age": 20, "grade": "A"},
{"name": "Bob", "age": 21}, # 'grade' キーがない
{"name": "Carol", "age": 19, "grade": "B"}
]
for student in students:
print(f"{student['name']}: {student['grade']}") # Bob で KeyErrorOutput:
Alice: A
Traceback (most recent call last):
File "example.py", line 7, in <module>
print(f"{student['name']}: {student['grade']}")
~~~~~~~^^^^^^^^^
KeyError: 'grade'get() にデフォルト値を指定して、存在しないキーを丁寧に扱いましょう。
# 正しいバージョン
students = [
{"name": "Alice", "age": 20, "grade": "A"},
{"name": "Bob", "age": 21},
{"name": "Carol", "age": 19, "grade": "B"}
]
for student in students:
grade = student.get("grade", "Not assigned")
print(f"{student['name']}: {grade}")Output:
Alice: A
Bob: Not assigned
Carol: B24.2.6) AttributeError: 無効な属性アクセス
AttributeError は、オブジェクトに存在しない属性(attribute)やメソッド(method)にアクセスしようとしたときに発生します。異なる型のメソッドを混同したり、属性名を打ち間違えたりしたときによく起こります。
# 例 1: 型に対して間違ったメソッドを使う
numbers = [1, 2, 3, 4, 5]
numbers.append(6) # これは動く - リストには append() がある
print(numbers) # Output: [1, 2, 3, 4, 5, 6]
text = "Hello"
text.append("!") # AttributeError: 'str' object has no attribute 'append'Output:
[1, 2, 3, 4, 5, 6]
Traceback (most recent call last):
File "example.py", line 6, in <module>
text.append("!")
^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'append'文字列には append() メソッドがありません。文字列はイミュータブル(immutable)だからです。連結や他の文字列メソッドを使う必要があります。
# 正しいバージョン
text = "Hello"
text = text + "!" # Concatenation
print(text) # Output: Hello!AttributeError はタイプミスでも発生します。
# 例 2: メソッド名のスペルミス
message = "Python Programming"
result = message.uppper() # AttributeError: 'str' object has no attribute 'uppper'Output:
Traceback (most recent call last):
File "example.py", line 2, in <module>
result = message.uppper()
^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'uppper'. Did you mean: 'upper'?Python 3.10+ では、正しいスペルを提案してくれることが多い点に注目してください。正しいメソッドは upper() です。
# 正しいバージョン
message = "Python Programming"
result = message.upper()
print(result) # Output: PYTHON PROGRAMMING24.2.7) ZeroDivisionError: 0 で割る
ZeroDivisionError は、数学的に未定義である 0 による除算をしようとしたときに発生します。ユーザー入力や計算値が想定外に 0 になった場合などによく起こります。
# 例 1: 直接 0 で割る
result = 10 / 0 # ZeroDivisionError: division by zeroOutput:
Traceback (most recent call last):
File "example.py", line 1, in <module>
result = 10 / 0
~~~^~~
ZeroDivisionError: division by zeroこれは切り捨て除算や剰余にも当てはまります。
# 例 2: 他の除算操作
a = 10 // 0 # ZeroDivisionError
b = 10 % 0 # ZeroDivisionError計算に絡む、より現実的な例です。
# 例 3: 平均を計算する
def calculate_average(numbers):
total = sum(numbers)
count = len(numbers)
return total / count
scores = [85, 90, 78, 92]
print(calculate_average(scores)) # Output: 86.25
empty_scores = []
print(calculate_average(empty_scores)) # ZeroDivisionErrorOutput:
86.25
Traceback (most recent call last):
File "example.py", line 9, in <module>
print(calculate_average(empty_scores))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "example.py", line 4, in calculate_average
return total / count
~~~~~~^~~~~~~
ZeroDivisionError: division by zero空のリストは長さが 0 のため 0 除算になります。必ずこの条件をチェックしましょう。
# 正しいバージョン
def calculate_average(numbers):
if len(numbers) == 0:
return 0 # Or return None, or raise a more descriptive error
total = sum(numbers)
count = len(numbers)
return total / count
scores = [85, 90, 78, 92]
print(calculate_average(scores)) # Output: 86.25
empty_scores = []
print(calculate_average(empty_scores)) # Output: 0これらのよくある例外タイプを理解すると、問題を素早く診断できるようになります。例外が表示されたとき、タイプ名がまず問題のカテゴリを教えてくれ、エラーメッセージが何が起きたのかの具体的な詳細を提供します。
24.3) トレースバックを詳しく読み解く
実行時例外が発生すると、Python は何が悪かったのかを伝えるだけでなく、プログラムがそこに至るまでの経緯を示す詳細な トレースバック(traceback) を提供します。効果的にデバッグするためには、トレースバックの読み方を学ぶことが不可欠です。トレースバックは、エラーに遭遇する前にプログラムがたどった道筋を示すパンくずリストのようなものです。
24.3.1) トレースバックの構造
まずはエラーを含む簡単な例から始め、トレースバックの各部分を詳しく見ていきましょう。
# エラーのあるシンプルなプログラム
def calculate_discount(price, discount_percent):
discount_amount = price * (discount_percent / 100)
final_price = price - discount_amount
return final_price
def process_order(item_price, discount):
discounted_price = calculate_discount(item_price, discount)
tax = discounted_price * 0.08
total = discounted_price + tax
return total
# メインプログラム
original_price = "50" # おっと! これは数値であるべきです
discount_rate = 10
final_cost = process_order(original_price, discount_rate)
print(f"Final cost: ${final_cost:.2f}")Output:
Traceback (most recent call last):
File "example.py", line 16, in <module>
final_cost = process_order(original_price, discount_rate)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "example.py", line 8, in process_order
discounted_price = calculate_discount(item_price, discount)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "example.py", line 2, in calculate_discount
discount_amount = price * (discount_percent / 100)
~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~
TypeError: can't multiply sequence by non-int of type 'float'このトレースバックの各要素を分解してみましょう。
1. ヘッダー: "Traceback (most recent call last):"
この行は、これから続く内容がトレースバック(関数呼び出しの記録)であることを示します。"most recent call last" は、トレースバックが時系列順に表示されていることを意味します。最初に呼び出された関数が最初に現れ、実際にエラーが起きた場所が最後に現れます。
2. コールスタック(上から下に読む):
File "example.py", line 16, in <module>
final_cost = process_order(original_price, discount_rate)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^これは連鎖の中の最初の関数呼び出しです。次の情報が示されています。
- ファイル名:
"example.py"- コードがある場所 - 行番号:
line 16- この呼び出しを行った正確な行 - コンテキスト:
in <module>- このコードがトップレベル(関数の内側ではない)にあること - コード: 実際に実行された行
- ハイライト:
^文字が、行の中で関係する特定部分を指している
<module> のコンテキストは、このコードが関数の内側ではなく、モジュールレベル(スクリプトのメイン部分)で動いていることを意味します。
File "example.py", line 8, in process_order
discounted_price = calculate_discount(item_price, discount)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^これは2 つ目の関数呼び出しです。process_order 関数が 16 行目から呼び出され、いまはその関数内の 8 行目にいて、そこで calculate_discount を呼び出しています。
File "example.py", line 2, in calculate_discount
discount_amount = price * (discount_percent / 100)
~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~ここがエラーが実際に発生した場所です。いまは calculate_discount 関数内の 2 行目で、問題を引き起こした行が示されています。
3. エラーメッセージ:
TypeError: can't multiply sequence by non-int of type 'float'これが実際に発生したエラーです。
- 例外タイプ:
TypeError- エラーのカテゴリを示します - 説明: それ以降が、具体的に何が起きたかを説明しています
このケースでは、(ここでは文字列の)シーケンスに float を掛けようとしたため、許可されていないと言われています。
24.3.2) トレースバックを下から上に読む
トレースバックは時系列順(上から下)に出力されますが、経験のあるプログラマーは実際のエラーが最下部にあるため、しばしば 下から上に読みます。上側の行は、そこに至るまでの経緯を示しています。
先ほどのトレースバックを下から上に読んでみましょう。
ステップ 1: エラーメッセージから始める
TypeError: can't multiply sequence by non-int of type 'float'「なるほど、シーケンスに float を掛けようとした。これは許されない。」
ステップ 2: エラーが起きた場所を見る
File "example.py", line 2, in calculate_discount
discount_amount = price * (discount_percent / 100)
~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~「エラーは 2 行目の calculate_discount 関数で起きている。price に何かを掛けている。」
ステップ 3: どうやってそこに来たかをたどる
File "example.py", line 8, in process_order
discounted_price = calculate_discount(item_price, discount)「calculate_discount は 8 行目の process_order から呼び出され、item_price が price 引数として渡されている。」
ステップ 4: さらにさかのぼる
File "example.py", line 16, in <module>
final_cost = process_order(original_price, discount_rate)「そして process_order はメインプログラムの 16 行目から呼び出され、original_price が item_price として渡されている。」
ステップ 5: 根本原因を見つける
これで原因が追えます。original_price は "50"(文字列)で、それが process_order の item_price に渡され、calculate_discount の price に渡され、そこで float を掛けようとしているのです。解決策は original_price を数値にすることです。
# 修正版
def calculate_discount(price, discount_percent):
discount_amount = price * (discount_percent / 100)
final_price = price - discount_amount
return final_price
def process_order(item_price, discount):
discounted_price = calculate_discount(item_price, discount)
tax = discounted_price * 0.08
total = discounted_price + tax
return total
# メインプログラム - 型を修正
original_price = 50 # 文字列ではなく数値になりました
discount_rate = 10
final_cost = process_order(original_price, discount_rate)
print(f"Final cost: ${final_cost:.2f}") # Output: Final cost: $48.60トレースバックの読み方を理解すると、威圧的な文字の壁だったものが、役立つデバッグツールへと変わります。各行はプログラムの実行経路に関する貴重な情報を提供しており、練習を重ねれば、トレースバックの導きに従って素早く問題を特定し、修正できるようになります。
24.4) 例外がプログラムの通常の流れをどう変えるか
例外が発生すると、単にプログラムが止まるだけでなく、プログラムの実行のされ方が根本的に変わります。この挙動を理解することは、堅牢なコードを書くためにも、エラーが起きたときに何が起きるのかを理解するためにも重要です。
24.4.1) 通常のプログラムフロー vs 例外フロー
通常の実行では、Python はコードを上から下へ、1 行ずつ実行します。
# 通常のプログラムフロー
print("Step 1: Starting calculation")
result = 10 + 5
print(f"Step 2: Result is {result}")
final = result * 2
print(f"Step 3: Final value is {final}")
print("Step 4: Program complete")Output:
Step 1: Starting calculation
Step 2: Result is 15
Step 3: Final value is 30
Step 4: Program completeすべての行が順番に実行されます。では例外が発生するとどうなるでしょうか。
# 例外がある場合のプログラムフロー
print("Step 1: Starting calculation")
result = 10 / 0 # ここで ZeroDivisionError が発生します
print(f"Step 2: Result is {result}") # これは実行されません
final = result * 2 # これは実行されません
print(f"Step 3: Final value is {final}") # これは実行されません
print("Step 4: Program complete") # これは実行されませんOutput:
Step 1: Starting calculation
Traceback (most recent call last):
File "example.py", line 2, in <module>
result = 10 / 0
~~~^~~
ZeroDivisionError: division by zero最初の print 文だけが実行されたことに注目してください。2 行目で例外が発生した瞬間に、Python は残りのコードの実行を停止しました。例外が通常の流れを中断したのです。
24.4.2) 例外はコールスタックをさかのぼって伝播する
関数の内部で例外が発生すると、Python はその関数内で止まるだけではなく、コールスタックを通って上方向に伝播(propagate)(移動)し、どこかで処理されるか、プログラムが終了するまで続きます。
# 例 1: 関数をまたいで例外が伝播する
def calculate_average(numbers):
total = sum(numbers)
count = len(numbers)
return total / count # ZeroDivisionError が発生する可能性がある
def process_scores(score_list):
print("Processing scores...")
avg = calculate_average(score_list)
print(f"Average calculated: {avg}")
return avg
def main():
print("Program starting")
scores = [] # 空のリスト
result = process_scores(scores)
print(f"Final result: {result}")
print("Program ending")
main()Output:
Program starting
Processing scores...
Traceback (most recent call last):
File "example.py", line 18, in <module>
main()
File "example.py", line 14, in main
result = process_scores(scores)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "example.py", line 9, in process_scores
avg = calculate_average(score_list)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "example.py", line 4, in calculate_average
return total / count
~~~~~~^~~~~~~
ZeroDivisionError: division by zero何が起きたのかを追ってみましょう。
main()の実行が始まり、"Program starting" を表示しましたmain()がprocess_scores()を呼び出しましたprocess_scores()が "Processing scores..." を表示しましたprocess_scores()がcalculate_average()を呼び出しましたcalculate_average()が 0 で割ろうとしました- 例外が発生し、上方向へ伝播しました:
calculate_average()はただちに停止しました(値を返しませんでした)- 制御は
process_scores()に戻りましたが、通常の戻り方ではなく、例外が伝播し続けました process_scores()はただちに停止しました(calculate_average()の後の print は実行されませんでした)- 制御は
main()に戻りましたが、同様に例外は伝播し続けました main()はただちに停止しました(process_scores()の後の print は実行されませんでした)
- プログラムはトレースバックとともに終了しました
どの関数においても、例外の後にあるコードは一切実行されませんでした。例外はすべての関数呼び出しを通って上へ「泡のように浮かび上がり」、トップレベルに到達してプログラムを終了させました。
24.5) デバッグ思考: エラーを失敗ではなく情報として扱う
プログラミングにおける最も重要なスキルの 1 つは、完璧なコードを書くことではなく、不完全なコードとうまく付き合う方法を学ぶことです。経験レベルに関係なく、すべてのプログラマーはエラーを生むコードを書きます。エラーに苦しむプログラマーと、効果的なプログラマーの違いは、エラーを避けることではなく、それにどう反応するかにあります。
24.5.1) エラーは失敗ではない
プログラミングを学んでいると、エラーに遭遇したときにイライラしてしまうのは自然なことです。何か間違ったことをした、あるいは自分は「理解できていない」と感じるかもしれません。この考え方は逆効果であり、さらに重要なことに正しくありません。
エラーは失敗ではなく、フィードバックです。
エラーは、GPS がルートを再計算するのに似ています。曲がるべきところを通り過ぎても、GPS は「失敗した!」とは言いません。「ルートを再計算します」と言って、新しい道順を示します。Python のエラーメッセージも同じで、たどった道ではうまくいかなかったことを伝え、うまくいく道を見つけるための情報を提供してくれます。
この簡単な例を考えてみましょう。
# 平均を計算する最初の試み
def calculate_average(numbers):
total = sum(numbers)
average = total / len(numbers)
return average
scores = []
result = calculate_average(scores)
print(f"Average: {result}")Output:
Traceback (most recent call last):
File "example.py", line 8, in <module>
result = calculate_average(scores)
^^^^^^^^^^^^^^^^^^^^^^^^^
File "example.py", line 4, in calculate_average
average = total / len(numbers)
~~~~~~^~~~~~~~~~~~~~
ZeroDivisionError: division by zeroこのエラーは、あなたがダメなプログラマーだと言っているのではありません。具体的で有用なことを言っています。「0 で割ろうとした。これはリストが空のときに起きる。つまり、そのケースを処理する必要がある。」
この情報があれば、コードを改善できます。
# エラーフィードバックに基づいて改善したバージョン
def calculate_average(numbers):
if len(numbers) == 0:
return 0 # Or return None, or raise a more descriptive error
total = sum(numbers)
average = total / len(numbers)
return average
scores = []
result = calculate_average(scores)
print(f"Average: {result}") # Output: Average: 0このエラーがより良いコードを書く助けになりました。エラーがなければ、関数が空のリストを扱えないことに気づかなかったかもしれません。
24.5.2) すべてのエラーは何かを教えてくれる
遭遇するエラーの一つひとつが、Python、あなたのコード、またはプログラミング全般について何かを教えてくれます。異なるエラーが何を教えてくれるのか、いくつか例を見てみましょう。
例 1: 型について学ぶ
# 互換性のない型を足そうとする
age = 25
message = "You are " + age + " years old"Output:
TypeError: can only concatenate str (not "int") to strこのエラーが教えてくれること: Python には厳密な型ルールがあります。連結で文字列と数値を混ぜることはできません。このエラーは型の互換性について教え、型変換という概念を導入してくれます。
例 2: データ構造について学ぶ
# 辞書をリストのようにアクセスしようとする
student = {"name": "Alice", "age": 20}
first_value = student[0]Output:
KeyError: 0このエラーが教えてくれること: 辞書は数値インデックスではなくキーを使います。このエラーは辞書とリストの違い、そして辞書の値に正しくアクセスする方法を教えてくれます。
例 3: スコープについて学ぶ
# 定義前に変数を使おうとする
def greet():
print(f"Hello, {name}!")
greet()
name = "Alice"Output:
NameError: name 'name' is not definedこのエラーが教えてくれること: 変数は使う前に定義されている必要があり、実行順序が重要です。このエラーは変数スコープと初期化の重要性を教えてくれます。
これらのエラーはいずれも、Python をよりよく理解するために役立つ、具体的で実行可能な情報を提供します。障害として見るのではなく、学びの機会として捉えましょう。
24.5.3) デバッグ思考を受け入れる
プロのプログラマーは、かなりの時間をデバッグに費やします。弱さの表れではなく、仕事の中核的な一部です。最高のプログラマーとは、ミスをしない人ではなく、次のような人たちです。
- エラーが起きることを想定している: エラーは起きるものだと分かっており、驚いたり落ち込んだりしません
- エラーを注意深く読む: エラーメッセージから最大限の情報を引き出します
- 体系的にデバッグする: ランダムに変更するのではなく、論理的なプロセスに従います
- エラーから学ぶ: 各エラーを Python をより深く理解する機会として使います
- 好奇心を持ち続ける: 「どう直す?」だけではなく「なぜこうなった?」と問いかけます
覚えておいてください。あらゆるエラーは、Python、プログラミング、または問題解決について新しいことを学ぶ機会です。エラーを価値あるフィードバックとして受け止め、体系的に取り組み、デバッグの成功を喜びましょう。この思考は、あなたのプログラミングの旅を通じて役に立ちます。
エラーとトレースバックを理解することは、効果的な Python プログラマーになるための基本です。この章では、構文エラー(コード構造の問題)と実行時例外(実行中の問題)を区別すること、よくある例外タイプとそれが示すものを認識すること、詳細なトレースバックを読み解いて問題の根本原因を見つけること、例外がコールスタックをさかのぼって伝播することでプログラムの流れをどう変えるかを理解すること、そしてエラーを失敗ではなく価値ある情報として扱うデバッグ思考を身につけることを学びました。
これらのスキルは次の章の土台になります。次の章では try と except ブロックを使って例外を丁寧に扱い、プログラムがエラーから回復して実行を継続できるようにする方法を学びます。しかし例外を効果的に扱う前に、まず例外を十分に理解する必要があります。そしてそれこそが、この章で達成したことです。