7. エラーと向き合う

目次

この講座で使用する Google Colab の URL

7. エラーと向き合う (Google Colab)

演習課題

Ex.7. エラーと向き合う (Google Colab)

この講座で使用する Python, Jupyter Notebook のファイルと実行環境

Lesson 7: high-school-python-code (GitHub)

7.1 エラーと例外:よくあるミスとその原因

私たちが日常生活の中で「失敗」をするように、プログラムを書いているときにも「失敗」が起こります。プログラミングの世界では、この「失敗」は主に「エラー」と「例外」の2種類に分類されます。

エラーと例外の違い

  • エラー:プログラムの実行前に検出される問題で、主に構文(文法)の誤りなどのことです。
  • 例外:プログラムの実行中に発生する予期しない状況のことです。

エラーの場合は実行前の構文の解析で発生するので、プログラムは全く実行されずに終了します。

例外の場合は、プログラムの実行中に発生するので、例外が発生したところまでは実行され、その後は中断されます。

どちらの場合も、Python はプログラムの実行を中断し、何が問題だったのかを教えてくれるメッセージを表示します。

主なエラーの種類と原因

1. 構文エラー (SyntaxError)

プログラムの「文法」が間違っているときに発生するエラーです。コードを実行する前に検出されます。

構文エラーの例

# カッコの閉じ忘れ
print("Hello World"  # 閉じカッコがない

# コロンの忘れ
if x > 5
    print("x は 5 より大きい")  # if 文の後のコロンがない

2. インデントエラー (IndentationError)

Pythonではインデント(字下げ)が構文の一部です。これが間違っているとエラーになります。

インデントエラーの例

if x > 5:
print("x は 5 より大きい")  # インデントがない

# または混在したインデント
if x > 5:
    print("正しいインデント")
   print("間違ったインデント")  # スペースの数が違う

主な例外の種類と原因

例外はプログラムの実行中に発生する問題です。

1. 名前エラー (NameError)

定義されていない変数や関数を使おうとしたときに発生します。

名前エラーの例

# 変数名のタイプミス
message = "こんにちは"
print(mesage)  # 正しくは message

# 定義前に使用
print(total)  # total はまだ定義されていない
total = 0

2. 型エラー (TypeError)

互換性のないデータ型で操作を行おうとしたときに発生します。

型エラーの例

# 文字列と数値の足し算
name = "山田"
age = 15
result = name + age  # 文字列と数値は直接足せない

# 数値でない物に掛け算
result = "hello" * "world"  # 文字列同士の掛け算はできない

3. インデックスエラー (IndexError)

リストや文字列の範囲外の要素にアクセスしようとしたときに発生します。

インデックスエラーの例

fruits = ["りんご", "バナナ", "オレンジ"]
print(fruits[3])  # インデックスは 0, 1, 2 しかない

name = "Python"
print(name[10])  # 文字列の長さは 6 なので 10 番目の文字はない

4. 属性エラー (AttributeError)

オブジェクトが持っていないメソッドや属性を使おうとしたときに発生します。

属性エラーの例

number = 42
number.append(10)  # 数値型には append メソッドはない

text = "Hello"
text.update()  # 文字列型には update メソッドはない

よくあるミスとその対策

プログラミング初心者がよくやってしまうミスと、その対策を見ていきましょう:

1. 変数名のタイプミス

タイプミスの例と対策

# 間違った例
counter = 0
conter = conter + 1  # counter のタイプミス

# 対策:変数名を統一し、わかりやすい命名を心がける
counter = 0
counter = counter + 1  # 正しく counter と書く

2. カッコの対応忘れ

カッコの対応忘れの例と対策

# 間違った例
print("Hello", (2 + 3 * 4)  # 閉じカッコが足りない

# 対策:エディタの括弧強調機能を使う
print("Hello", (2 + 3 * 4))  # 正しく閉じカッコを入れる

3. インデントのズレ

インデントのズレの例と対策

# 間違った例
if x > 5:
    print("x は 5 より大きい")
   print("処理を続けます")  # インデントが揃っていない

# 対策:インデントを揃える
if x > 5:
    print("x は 5 より大きい")
    print("処理を続けます")  # インデントを揃える

4. 文字列と数値の混在

型の混在の例と対策

# 間違った例
age = input("年齢を入力してください: ")
birth_year = 2023 - age  # input は文字列を返すので計算できない

# 対策:適切な型変換を行う
age = input("年齢を入力してください: ")
birth_year = 2023 - int(age)  # int() で数値に変換

7.2 try-except でエラーに負けないプログラム作り

前回のセクションでは、エラーの種類と原因について学びました。しかし、プログラムを実際に運用する場面では「エラーが発生しても、プログラムを停止させずに処理を続行したい」ということがよくあります。

例えば、ユーザーが間違った入力をしたとき、プログラムが強制終了するのではなく、「間違っていますよ」と教えて再入力を促す方が親切ですよね。このような「エラー処理」を行うのが try-except 構文です。

try-except の基本

try-except は、「試してみて、もし失敗したら別の方法を取る」という日常的な考え方をプログラムで実現する方法です。基本的な構造は次のとおりです。

try-except の基本構造

try:
    # エラーが発生するかもしれない処理
    # この部分を「試してみる」
    pass
except:
    # エラーが発生した場合の処理
    # 「失敗した場合の対応策」
    pass

基本的な使い方

実際に簡単な例で見てみましょう。ユーザーに数字を入力してもらうケースを考えます:

基本的な try-except

try:
    # エラーが発生するかもしれない処理
    number = int(input("数字を入力してください:"))
    print(f"入力された数字は {number} です")
except ValueError:
    # 数値に変換できないときの処理
    print("数字以外が入力されました")

上記のコードでは:

  1. ユーザーの入力を int() で数値に変換しようとします
  2. もし入力が数字でなかった場合、ValueError が発生します
  3. except ValueError: の部分で、そのエラーを捕まえて対処します

これにより、ユーザーが「abc」などの文字列を入力しても、プログラムは停止せず「数字以外が入力されました」というメッセージを表示します。

複数の例外を扱う

実際のプログラムでは、複数の種類のエラーが発生する可能性があります。それぞれのエラーに対して異なる対応をしたい場合は、複数の except 節を使います:

複数のエラーを扱う

try:
    num1 = int(input("1つ目の数字:"))
    num2 = int(input("2つ目の数字:"))
    result = num1 / num2
    print(f"結果:{result}")
except ValueError:
    # 数値への変換に失敗した場合
    print("数字を入力してください")
except ZeroDivisionError:
    # 0 で割ろうとした場合
    print("0 で割ることはできません")
except Exception:
    # その他のすべてのエラー
    print("予期せぬエラーが発生しました")

このプログラムでは、3 種類のエラーに対応しています:

  1. ValueError: 数字以外が入力された場合
  2. ZeroDivisionError: 0 で割ろうとした場合
  3. その他のエラー: 想定外のエラーが発生した場合

複数の例外を扱う場合は、if 文の条件判定と同じように、上から順番にチェックされます。一つの except 節に入ったら、その下の except 節は実行されません。

else と finally

try-except には、さらに elsefinally を追加することができます。

else と finally を使った完全な形

try:
    number = int(input("数字を入力してください:"))
except ValueError:
    print("数字以外が入力されました")
else:
    # エラーが発生しなかった場合に実行される
    print(f"正しく数字 {number} が入力されました")
finally:
    # エラーの有無にかかわらず必ず実行される
    print("処理を終了します")
  • else: try ブロックでエラーが発生しなかった場合のみ実行されます
  • finally: エラーの有無にかかわらず、最後に必ず実行される部分です

7.3 エラーメッセージを読んで問題箇所を特定する

これまでの学習で、エラーの基本とその対処法としての try-except について学びました。

最も重要なスキルの一つは「エラーメッセージを正確に読み解く力」です。エラーメッセージは一見難しく見えますが、実は問題解決のための貴重な情報が詰まっています。

エラーメッセージの構造を理解する

Python のエラーメッセージは一般的に以下の要素で構成されています:

  1. エラーの種類(例:TypeError, ValueError)
  2. エラーの説明文
  3. 発生場所(ファイル名と行番号)
  4. 問題を含むコードの表示

実際のエラーメッセージを見てみましょう:

Traceback (most recent call last):
  File "example.py", line 15, in <module>
    text = "答え:" + 42
TypeError: can only concatenate str (not "int") to str

このエラーメッセージを分解すると:

  1. エラーの種類: TypeError
  2. 説明文: can only concatenate str (not "int") to str(文字列には文字列しか連結できない)
  3. 発生場所: example.py の 15 行目
  4. 問題コード: text = "答え:" + 42

これらの情報を組み合わせると、「文字列と整数を直接連結しようとしているため、型エラーが発生している」ことがわかります。

エラーメッセージを読み解く練習

いくつかのエラーメッセージを読み解く練習をしましょう。

例 1: インデックスエラー

Traceback (most recent call last):
  File "example.py", line 8, in <module>
    print(fruits[3])
IndexError: list index out of range

読み解き:

  • エラーの種類: IndexError(インデックスエラー)
  • 説明文: list index out of range(リストの範囲外のインデックス)
  • 発生場所: example.py の 8 行目
  • 問題コード: print(fruits[3])

原因:リスト fruits の長さが 3 以下なのに、インデックス 3(4番目の要素)にアクセスしようとしています。

例 2: 名前エラー

Traceback (most recent call last):
  File "example.py", line 12, in <module>
    total = price * quantity
NameError: name 'quantity' is not defined

読み解き:

  • エラーの種類: NameError(名前エラー)
  • 説明文: name 'quantity' is not defined(変数 'quantity' が定義されていない)
  • 発生場所: example.py の 12 行目
  • 問題コード: total = price * quantity

原因:quantity という変数を使おうとしていますが、その変数が事前に定義されていません。

エラーメッセージから問題を修正する

エラーメッセージを理解できたら、次は問題を修正しましょう。先ほどの例を修正してみます:

例 1(修正版): インデックスエラーの修正

インデックスエラーの修正

# エラーコード
fruits = ["りんご", "バナナ", "オレンジ"]
print(fruits[3])  # IndexError: list index out of range

# 修正方法 1: 正しいインデックスを使用
print(fruits[2])  # オレンジ(3番目の要素はインデックス2)

# 修正方法 2: インデックスの範囲をチェック
if len(fruits) > 3:
    print(fruits[3])
else:
    print("インデックスが範囲外です")

例 2(修正版): 名前エラーの修正

名前エラーの修正

# エラーコード
price = 1200
total = price * quantity  # NameError: name 'quantity' is not defined

# 修正方法: 変数を事前に定義する
price = 1200
quantity = 3  # 変数を定義
total = price * quantity  # これで正しく動作

実際の例: 文字列と整数の連結エラー

最初に挙げた例の修正方法を見てみましょう:

型エラーの修正

# エラーコード
text = "答え:" + 42  # TypeError: can only concatenate str (not "int") to str

# 修正方法 1: 整数を文字列に変換
text = "答え:" + str(42)  # str() で整数を文字列に変換

# 修正方法 2: f文字列を使用
text = f"答え:{42}"  # f文字列なら型を気にする必要がない

7.4 print デバッグでコードを細かくチェック

先ほど、エラーメッセージの読み方を学びました。しかし、中には「エラーは出ないけれど、期待した結果にならない」という状況もよくあります。このような「論理エラー」を見つけるために有効な手法が「print デバッグ」です。

print デバッグとは?

print デバッグとは、プログラムの実行中に変数の値や実行フローを print 文を使って出力し、プログラムの動きを可視化する方法です。これにより、どこで予想と異なる動作をしているのかを特定しやすくなります。

基本的な print デバッグの例

簡単な例で print デバッグを見てみましょう:

基本的な print デバッグ

def calculate_total(items):
    total = 0
    print(f"計算開始:items = {items}")  # デバッグ用出力

    for item in items:
        print(f"処理中:item = {item}")  # 各ステップの確認
        total += item
        print(f"現在の合計:{total}")    # 中間結果の確認

    print(f"計算終了:total = {total}")  # 最終結果の確認
    return total

# プログラムの実行
numbers = [10, 20, 30]
result = calculate_total(numbers)

出力:

計算開始:items = [10, 20, 30]
処理中:item = 10
現在の合計:10
処理中:item = 20
現在の合計:30
処理中:item = 30
現在の合計:60
計算終了:total = 60

このコードでは、計算の各ステップで変数の値を出力しています。これにより、プログラムがどのように動作しているかを詳細に追跡できます。

効果的な print デバッグのテクニック

1. 目印をつける

print 文の出力に目印をつけると、多くの出力の中から見つけやすくなります:

目印をつけた print デバッグ

x = 10
y = 20

print("===== [DEBUG] 関数開始 =====")
print(f"[DEBUG] 変数 x の値: {x}")
print(f"[DEBUG] 変数 y の値: {y}")
print("===== [DEBUG] 関数終了 =====")

2. 変数の型も確認する

予期せぬエラーの原因として、変数の型が想定と異なる場合があります:

変数の型を確認する

value = input("数値を入力: ")
print(f"[DEBUG] value の値: {value}, 型: {type(value)}")
# 出力例: [DEBUG] value の値: 123, 型: <class 'str'>

# 数値計算の前に型を確認し、必要に応じて変換
if isinstance(value, str):
    value = int(value)

3. 条件分岐の流れを追跡する

if-else 文などの条件分岐がどのように実行されているかを確認します:

条件分岐の追跡

score = 75

print(f"[DEBUG] score の値: {score}")

if score >= 90:
    print("[DEBUG] 条件1が True: score >= 90")
    grade = "A"
elif score >= 80:
    print("[DEBUG] 条件2が True: score >= 80")
    grade = "B"
elif score >= 70:
    print("[DEBUG] 条件3が True: score >= 70")
    grade = "C"
else:
    print("[DEBUG] すべての条件が False")
    grade = "D"

print(f"[DEBUG] 最終的な grade: {grade}")

4. ループの各反復を追跡する

ループ処理の各ステップで何が起きているかを確認します:

ループの追跡

numbers = [10, -5, 20, -8, 15]
positive_sum = 0

print(f"[DEBUG] 入力リスト: {numbers}")

for i, num in enumerate(numbers):
    print(f"[DEBUG] ループ {i+1}回目: num = {num}")

    if num > 0:
        print(f"[DEBUG] 正の数を検出: {num}")
        positive_sum += num
        print(f"[DEBUG] 現在の positive_sum: {positive_sum}")

print(f"[DEBUG] 最終的な正の数の合計: {positive_sum}")

一時的な print デバッグとコメントアウト

デバッグが終わったら、忘れずに print 文を削除するか、コメントアウトしましょう!

コメントアウトした print デバッグ

def calculate_total(items):
    total = 0
    # print(f"計算開始:items = {items}")  # デバッグ用出力

    for item in items:
        # print(f"処理中:item = {item}")  # 各ステップの確認
        total += item
        # print(f"現在の合計:{total}")    # 中間結果の確認

    # print(f"計算終了:total = {total}")  # 最終結果の確認
    return total

7.5 コメントや整形でバグを減らす工夫

これまでの学習では、エラーが発生した後の対処法について学んできました。しかし、「バグが発生しにくいコードを書く」ことも同じくらい重要です。

プログラムを書くとき、コードの読みやすさを意識するだけでバグを大幅に減らすことができます。

可読性の高いコードとは?

可読性の高いコード(読みやすいコード)とは、「他の人(そして数週間後の自分自身)」が読んだときに、すぐに理解できるコードのことです。

  1. 適切な命名をしている(変数名、関数名など)
  2. 分かりやすいコメントを書く
  3. コードがきれいに整形されている(インデントやスペース)
  4. 適切に関数やモジュールを分割する

のような、分かりやすく読みやすいコードのことです。

悪い例と良い例の比較

まず、同じ機能を持つコードの「悪い例」と「良い例」を比較してみましょう:

読みにくいコード(悪い例)

def calc(x,y,z):
    a=x+y
    if a>10:b=a*z
    else:b=a/z
    return b


print(calc(1, 2, 3))

このコードはきちんと動作はしますが、以下のような問題があります。

  • 変数名や関数名が、何を意味するのか分からない
  • スペースがなく、読みにくい
  • コメントがない
  • インデントが見づらい(if-else 文)

同じ機能をより読みやすく書き直した例を見てみましょう。

読みやすいコード(良い例)

def calculate_result(base_num: int, add_num: int, multiplier: int) -> int | float:
    """2 つの数の和を計算し、条件に応じて乗算または除算を行う.

    Args:
        base_num: 基準となる数
        add_num: 加算する数
        multiplier: 乗除算に使う数

    Returns:
        int: 計算結果
    """
    # 最初の 2 つの数を加算
    sum_result = base_num + add_num

    # 合計が 10 より大きければ乗算、それ以外は除算
    if sum_result > 10:
        final_result = sum_result * multiplier
    else:
        final_result = sum_result / multiplier

    return final_result


print(calculate_result(1, 2, 3))

改善されたポイント:

  • 意味のある変数名と関数名を使用
  • 関数の目的と引数、戻り値を説明する Docstring
  • 処理の流れを説明するコメント
  • 適切なインデントと空白
  • 1 行ごとに 1 つの処理を書くことで流れが分かりやすい

良い名付け方

変数名や関数名は、そのコードの意図を明確に表すものにすると良いです。

変数名の命名規則

変数名の良い例と悪い例

# 悪い例
a = 15  # 何を表す変数か分からない
lst = [10, 20, 30]  # 中身が分からない
tmp = "東京都"  # 一時的な変数なのか、何のデータなのか不明

# 良い例
age = 15  # 年齢であることが明確
scores = [10, 20, 30]  # 点数のリストであることが分かる
city_name = "東京都"  # 都市名であることが分かる

関数名の命名規則

関数名の良い例と悪い例

# 悪い例
def calc(scores):  # 何を計算するのか不明
    return sum(scores) / len(scores)

# 良い例
def calculate_average(scores):  # 平均値を計算することが明確
    return sum(scores) / len(scores)

効果的なコメントの書き方

コメントは、コードだけでは分かりにくい「なぜそうしているのか」という背景や意図を説明するために使います。

コメントの種類

  1. 関数のドキュメンテーション文字列(docstring): 関数の目的、引数、戻り値などを説明

ドキュメンテーション文字列の例

def calculate_discount(price, discount_rate):
    """
    商品の割引後価格を計算する関数

    Args:
        price: 元の価格
        discount_rate: 割引率(0.0〜1.0)

    Returns:
        割引後の価格(小数点以下切り捨て)
    """
    discounted_price = price * (1 - discount_rate)
    return int(discounted_price)
  1. コード説明コメント: コードブロックの目的や動作を説明

コード説明コメントの例

# ユーザー入力の値を数値に変換し、エラー処理を行う
try:
    user_input = input("数値を入力してください: ")
    number = float(user_input)
except ValueError:
    print("有効な数値を入力してください")
    number = 0
  1. 警告・注意コメント: 注意が必要な箇所や潜在的な問題を示す

警告コメントの例

def fetch_data_from_api(user_id: str) -> None:
    """API からデータを取得する関数.

    Args:
        user_id: ユーザーの ID
    """
    pass


# 注意: この API は 1 分間に最大 60 回までしか呼び出せません
# 頻繁に呼び出すとエラーになる可能性があります
fetch_data_from_api(user_id="1234567890")

コードの整形とスタイル

コードの見た目(整形)も可読性に大きく影響します。Python には「PEP 8」というコードスタイルのガイドラインがあります。

インデントと空白

インデントと空白の例

# 悪い例 (インデントが不揃い、空白がない)
def calculate(a,b):
   result=a+b
   if result>10:
        return result*2
   else:
    return result

# 良い例 (一貫したインデント、適切な空白)
def calculate(a, b):
    result = a + b

    if result > 10:
        return result * 2
    else:
        return result

空行による区切り

関連する処理のブロックを空行で区切ると、コードの構造が分かりやすくなります。

空行による区切りの例

# 入力を受け取る
name = input("名前を入力してください: ")
age = int(input("年齢を入力してください: "))

# 条件に基づいて処理
if age >= 20:
    message = f"{name}さんは成人です"
else:
    message = f"{name}さんは未成年です"

# 結果を表示
print(message)

7.6 あえてエラーを起こして練習する

ここまでエラーの種類や対処法について学んできましたが、実際にエラーを体験してみることが理解を深める一番の方法です。「エラーを恐れない」ためには、あえてエラーを起こしてみて、その挙動を観察することが効果的です。

この章では、意図的に様々なエラーを発生させて、エラーメッセージの読み方や対処法を実践的に学びましょう。

基本的なエラー

まずは、基本的なエラーをあえて発生させて、try-except で捕捉するプログラムを見てみましょう:

基本的なエラー

def practice_errors():
    # 1. NameError: 定義されていない変数を使用
    try:
        print(undefined_name)  # この変数は定義されていない
    except NameError as e:
        print(f"変数が定義されていません:{e}")
        print(f"エラーの種類:{type(e).__name__}")
        print("--------------------")

    # 2. TypeError: 異なる型の演算
    try:
        result = "hello" + 123  # 文字列と数値は直接足せない
    except TypeError as e:
        print(f"型が合っていません:{e}")
        print(f"エラーの種類:{type(e).__name__}")
        print("--------------------")

    # 3. IndexError: リストの範囲外にアクセス
    try:
        list_data = [1, 2, 3]
        print(list_data[10])  # リストの範囲外のインデックス
    except IndexError as e:
        print(f"リストの範囲外です:{e}")
        print(f"エラーの種類:{type(e).__name__}")
        print("--------------------")


# 実行
practice_errors()

実行結果(例):

変数が定義されていません:name 'undefined_name' is not defined
エラーの種類:NameError
--------------------
型が合っていません:can only concatenate str (not "int") to str
エラーの種類:TypeError
--------------------
リストの範囲外です:list index out of range
エラーの種類:IndexError
--------------------

このプログラムでは、わざと 3 種類のエラーを発生させて、それぞれ try-except で捕捉しています。エラーメッセージだけでなく、エラーの種類も表示することで、どのようなエラーが発生したのかを明確にしています。

例外クラスの階層

Python の例外(エラー)は階層構造になっています。これを理解するために、例外の階層を視覚的に確認してみましょう。

以下のコードは、組み込み例外の階層を表示するプログラムです。ちょっと難しいコードなので、今理解できなくても大丈夫です。

例外クラスの階層を確認

def explore_exception_hierarchy():
    # 例外クラスの階層を確認
    import builtins

    # 組み込み例外の一覧を取得
    exception_classes = []

    for name in dir(builtins):
        obj = getattr(builtins, name)
        if isinstance(obj, type) and issubclass(obj, BaseException):
            exception_classes.append(obj)

    # 例外の階層を表示
    for exc_class in exception_classes:
        if exc_class.__base__ is BaseException or exc_class.__base__ is Exception:
            print(f"{exc_class.__name__}")

            # サブクラスを表示
            for sub_class in exception_classes:
                if sub_class.__base__ is exc_class and sub_class is not exc_class:
                    print(f"  └─ {sub_class.__name__}")


explore_exception_hierarchy()

このような表示結果になります。

ArithmeticError
  └─ FloatingPointError
  └─ OverflowError
  └─ ZeroDivisionError
AssertionError
AttributeError
BaseExceptionGroup
  └─ ExceptionGroup
BufferError
EOFError
OSError
  └─ BlockingIOError
  └─ ChildProcessError
  └─ ConnectionError
  └─ FileExistsError
  └─ FileNotFoundError
  └─ InterruptedError
  └─ IsADirectoryError
  └─ NotADirectoryError
  └─ PermissionError
  └─ ProcessLookupError
  └─ TimeoutError
Exception
  └─ ArithmeticError
  └─ AssertionError
  └─ AttributeError
  └─ BufferError
  └─ EOFError
  └─ OSError
  └─ OSError
  └─ ImportError
  └─ LookupError
  └─ MemoryError
  └─ NameError
  └─ OSError
  └─ ReferenceError
  └─ RuntimeError
  └─ StopAsyncIteration
  └─ StopIteration
  └─ SyntaxError
  └─ SystemError
  └─ TypeError
  └─ ValueError
  └─ Warning
GeneratorExit
OSError
  └─ BlockingIOError
  └─ ChildProcessError
  └─ ConnectionError
  └─ FileExistsError
  └─ FileNotFoundError
  └─ InterruptedError
  └─ IsADirectoryError
  └─ NotADirectoryError
  └─ PermissionError
  └─ ProcessLookupError
  └─ TimeoutError
ImportError
  └─ ModuleNotFoundError
KeyboardInterrupt
LookupError
  └─ IndexError
  └─ KeyError
MemoryError
NameError
  └─ UnboundLocalError
OSError
  └─ BlockingIOError
  └─ ChildProcessError
  └─ ConnectionError
  └─ FileExistsError
  └─ FileNotFoundError
  └─ InterruptedError
  └─ IsADirectoryError
  └─ NotADirectoryError
  └─ PermissionError
  └─ ProcessLookupError
  └─ TimeoutError
ReferenceError
RuntimeError
  └─ NotImplementedError
  └─ RecursionError
StopAsyncIteration
StopIteration
SyntaxError
  └─ IndentationError
SystemError
SystemExit
TypeError
ValueError
  └─ UnicodeError
Warning
  └─ BytesWarning
  └─ DeprecationWarning
  └─ EncodingWarning
  └─ FutureWarning
  └─ ImportWarning
  └─ PendingDeprecationWarning
  └─ ResourceWarning
  └─ RuntimeWarning
  └─ SyntaxWarning
  └─ UnicodeWarning
  └─ UserWarning

このプログラムでは、Python の組み込み例外のクラス階層を表示し、例外の子クラスと親クラスの関係を示しています。

例えば、ZeroDivisionErrorArithmeticError の子クラスです。「算術についてのエラーは色々ありますが、その中の一つが 0 で割ったときのエラー」ということです。

このような階層構造を理解することで、エラーをより適切に捕捉できるようになります。

そもそも、クラス (class) とは?

クラスは、オブジェクト指向プログラミングの基本概念です。オブジェクト指向プログラミングでは、プログラムをオブジェクトという単位で分割して考えます。

オブジェクトは、属性(データ)とメソッド(処理)を持つものです。

例えば、Student クラスは、学生の名前や年齢などの属性と、成績を計算するメソッドを持つものです。

Student クラスの例

class Student:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    def calculate_grade(self, score: int) -> str:
        """点数から成績を計算するメソッド.

        Args:
            score (int): 点数

        Returns:
            str: 成績
        """
        if score < 0 or score > 100:
            raise ValueError("点数は0〜100の範囲で入力してください")

        if score >= 80:
            return "A"
        elif score >= 70:
            return "B"
        elif score >= 60:
            return "C"
        else:
            return "D"

このクラスは、Student という名前のオブジェクトを作るための設計図です。

この設計図に従って、Student オブジェクトを作ることができます。オブジェクト自身を参照するために、self というキーワードを使います。

Student オブジェクトの作成

student_1 = Student("John", 20)
student_2 = Student("Michel", 22)

これを親クラスとして、このクラスを継承した HighSchoolStudent クラス(子クラス)を作ることができます。

HighSchoolStudent クラスの作成

class HighSchoolStudent(Student):
    def __init__(self, name: str, age: int, grade: int):
        super().__init__(name, age)
        self.grade = grade

このように、HighSchoolStudent クラスは Student クラスを継承しているため、Student クラスの属性とメソッドをそのまま使うことができます。

また、追加の属性として、grade を持つことができます。

HighSchoolStudent オブジェクトの作成

high_school_student = HighSchoolStudent("John", 20, 3)

7.7 テスト用のチェックを追加する

これまでの章では、エラーの理解と対処法について学んできました。今回は、エラーを事前に防ぐための重要な手法「テスト」について学びましょう。テストを書くことで、プログラムが正しく動作することを確認し、バグを早期に発見できます。

テストの重要性

プログラムを書くとき、私たちは「このコードは正しく動くはず」と思いながら書いています。しかし、思い込みによる間違いや見落としは誰にでも起こりえます。そこで活躍するのが「テスト」です。

テストには以下のようなメリットがあります:

  1. バグを早期に発見できる
  2. コードの品質を保証できる
  3. コードの変更による影響(副作用)を素早く検出できる
  4. プログラムの仕様を明確に理解できる

簡単なテストを書いてみよう

最も基本的なテスト方法は、assert 文を使うことです。assert は「表明する、主張する」という意味で、「この条件は真であるはず」と主張するものです。

基本的なテスト

def test_calculation_functions():
    """計算関数のテスト"""

    # 足し算のテスト
    assert add(2, 3) == 5, "2 + 3 は 5 になるはず"
    assert add(-1, 1) == 0, "-1 + 1 は 0 になるはず"

    # 掛け算のテスト
    assert multiply(2, 3) == 6, "2 * 3 は 6 になるはず"
    assert multiply(0, 5) == 0, "0 * 5 は 0 になるはず"

    print("すべてのテストが成功しました!")


def add(a, b):
    return a + b


def multiply(a, b):
    return a * b


try:
    test_calculation_functions()
except AssertionError as e:
    print(f"テストが失敗しました:{e}")

この例では、add 関数と multiply 関数が正しく動作することを確認するテストを書いています。

assert の後に条件式を書き、その条件が False の場合にエラーを発生させます。第二引数のメッセージは、エラー発生時に表示されるものです。

doctest モジュールを使ったテスト

Python には doctest という標準ライブラリがあり、ドキュメントにテストを書くことができます。

doctest を使ったテスト

import doctest


def add(a, b):
    """
    2つの数を足して返す関数.

    >>> add(1, 2)
    3
    """
    return a + b


if __name__ == "__main__":
    doctest.testmod(verbose=True)

このように、Docstring にテストを書くことができます。

unittest モジュールを使ったテスト

Python には unittest という標準ライブラリがあり、より体系的なテストを書くことができます。

unit とは、「単位」という意味の英単語で、ユニットテストは、プログラムの最小単位である関数やメソッドをテストするものです。

unittest を使ったテスト

import unittest


class TestCalculationFunctions(unittest.TestCase):
    def test_add(self):
        """add 関数のテスト"""
        self.assertEqual(add(2, 3), 5)
        self.assertEqual(add(-1, 1), 0)

    def test_multiply(self):
        """multiply 関数のテスト"""
        self.assertEqual(multiply(2, 3), 6)
        self.assertEqual(multiply(0, 5), 0)

    def test_average(self):
        """average 関数のテスト"""
        self.assertEqual(average([1, 2, 3]), 2)
        self.assertEqual(average([5, 5, 5]), 5)
        self.assertEqual(average([]), 0)


def add(a, b):
    return a + b


def multiply(a, b):
    return a * b


def average(numbers):
    if not numbers:
        return 0

    return sum(numbers) / len(numbers)


if __name__ == "__main__":
    # 通常のプログラムの場合
    # unittest.main()

    # Google Colab で実行する場合
    unittest.main(argv=['first-arg-is-ignored'], exit=False)

Ex.1 入力ミスがあっても止まらず再入力を促す計算プログラム

入力ミスがあっても止まらず再入力を促す計算プログラム

def get_number(prompt: str) -> int:
    """数値を安全に入力させる関数."""
    while True:
        try:
            value = int(input(prompt))
            return value
        except ValueError:
            print("数字を入力してください")


def safe_calculator() -> None:
    """エラーに強い電卓プログラム."""
    while True:
        try:
            # 数値の入力
            num1 = get_number("1つ目の数字を入力:")
            num2 = get_number("2つ目の数字を入力:")

            # 演算子の入力
            operator = input("演算子を入力(+, -, *, /):")

            # 計算実行
            if operator == "+":
                result = num1 + num2
            elif operator == "-":
                result = num1 - num2
            elif operator == "*":
                result = num1 * num2
            elif operator == "/":
                if num2 == 0:
                    print("0での割り算はできません")
                    continue
                result = num1 / num2
            else:
                print("無効な演算子です")
                continue

            # 結果表示
            print(f"結果:{result}")

            # 継続確認
            if input("続けますか?(y/n):") != "y":
                break

        except Exception as e:
            print(f"エラーが発生しました:{e}")
            print("もう一度試してください")


safe_calculator()

チャレンジ:

  • 小数点を含む数値(float)も処理できるようにしてみましょう
  • べき乗 (^) や剰余 (%) などの追加演算子をサポートしてみましょう
  • 計算履歴を保存する機能を追加してみましょう

Ex.2 存在しないファイルを開こうとしたときにエラーを回避するプログラム

存在しないファイルを開こうとしたときにエラーを回避するプログラム

class VirtualFileSystem:
    """仮想ファイルシステムクラス."""

    def __init__(self) -> None:
        # 仮想ファイルディレクトリ
        self.files = {
            "sample.txt": "これはサンプルファイルです。\n複数行のテキストを含んでいます。\nPythonプログラミングは楽しいですね。",
            "data.csv": "名前,年齢,職業\n田中,25,エンジニア\n佐藤,30,デザイナー\n鈴木,28,マーケター",
            "secret.txt": "このファイルはアクセス権限が必要です。"
        }

        # ファイルのアクセス権限 (True ならアクセス可能)
        self.permissions = {
            "sample.txt": True,
            "data.csv": True,
            "secret.txt": False
        }

    def list_files(self) -> None:
        """利用可能なファイル一覧を表示."""
        print("利用可能なファイル一覧:")
        for filename in self.files.keys():
            print(f"- {filename}")

    def read_file(self, filename: str) -> str | None:
        """ファイルを安全に読み込む."""
        try:
            if filename not in self.files:
                raise FileNotFoundError(f"ファイル '{filename}' が見つかりません")

            return self.files[filename]

        except FileNotFoundError as e:
            print(str(e))
            return None
        except Exception as e:
            print(f"予期せぬエラーが発生しました:{e}")
            return None


def process_file() -> None:
    """ファイル処理プログラム."""
    # 仮想ファイルシステムの初期化
    vfs = VirtualFileSystem()

    while True:
        print("\n")
        vfs.list_files()
        filename = input("\n処理するファイル名を入力してください:")

        # ファイルの読み込み
        content = vfs.read_file(filename)

        if content is not None:
            # ファイルの内容を処理
            print("\nファイルの内容:")
            print(content)

        # 継続確認
        if input("\n他のファイルを処理しますか?(y/n):") != "y":
            break


process_file()

チャレンジ:

  • ファイルの文字数や行数を表示する機能を追加してみましょう
  • ファイルの権限がなかった場合の例外 PermissionError をキャッチできるようにしてみましょう
  • ファイルの拡張子を入力して、その拡張子のファイルの一覧を表示するメソッドを作ってみましょう

まとめ

この章で学んだこと:

  • エラーは学習の機会として捉える
  • try-except でエラーに対応できる
  • デバッグ方法の使い分け
  • コードの整理でバグを防ぐ
  • テストでプログラムの品質を保つ