# Pythonの例外：try、except、else、finally、raise、独自の例外

URL: https://skillaria.top/ja/guides/python/isklyucheniya-python

Pythonの例外は、エラーを表すオブジェクトで、try/except で捕まえるまでプログラムの通常の流れを止めます。else は例外が起きなかったときに、finally は必ず実行されます。自分で例外を起こすには raise を使います。例：raise ValueError("金額が負の値です")。独自の例外は Exception を継承したクラスとして作ります。

Pythonの例外は、エラーが起きたことを知らせるオブジェクトで、プログラムの通常の流れを中断させます。誰も捕まえなければ、Pythonはtracebackを表示して終了します。例外を捕まえるには `try/except` を使い、自分で発生させるには `raise` を使います。ここでは、その仕組み、よくあるエラーの種類、そしてエラー処理がエラーの隠蔽にならないためのコツを見ていきます。

## Pythonの例外とは

ゼロ除算、存在しないキーへのアクセス、文字列を数値に変換する処理など、操作が成功しないとき、Pythonは例外オブジェクトを作って「送出」します。現在の関数の実行は止まり、例外は呼び出しの連鎖をさかのぼり、適切なハンドラが見つかるまで進みます。どこでも捕まえられなければ、プログラムはクラッシュします。

```
>>> 10 / 0
Traceback (most recent call last):
  File "", line 1, in
ZeroDivisionError: division by zero
```

よくある質問への答えを書いておきます。Pythonでゼロで割ると、`ZeroDivisionError` という例外が発生します。これは `/`、`//`、`%` のいずれでも同じです。

## tracebackの読み方

tracebackとは、プログラムがエラーに至るまでの経路を示すレポートです。読むときは下から上へ進みます。最後の行が最も重要で、例外の型とメッセージが書かれています。その上には、エラーが起きたコードの行、さらにその上には、その行を呼び出した場所が続き、エントリポイントまでさかのぼります。

```
Traceback (most recent call last):
  File "tracker.py", line 9, in
    total = parse_amount(row)
  File "tracker.py", line 4, in parse_amount
    return float(raw.strip())
           ~~~~~^^^^^^^^^^^^^
ValueError: could not convert string to float: '百'
```

Python 3.11以降では、コードの行の下に `~~~^^^` という印が表示されます（PEP 657）。どの式で失敗したのかを示してくれるので、1行に関数呼び出しが続いている場合にとても役立ちます。

### 「Did you mean」のヒント

Python 3.10以降では、名前を打ち間違えると、近い候補を提案してくれます。Python 3.15では、そのヒントがさらに賢くなりました。他の言語での習慣も認識します。`list.push` と書くと `append` を、`"coffee".toUpperCase()` と書くと `upper` を提案します。ヒントは入れ子のフィールドにも及びます。辞書が `totals` という属性に入っているオブジェクトのメソッドを `items()` と直接呼んでしまった場合、`.totals.items` を提案します。近い名前の候補は、`delattr()` でも表示されるようになりました。

## Pythonの例外の種類

組み込みの例外は数十種類ありますが、入門の段階で頻繁に目にするのは次のものです。

| 例外 | 発生する場面 | 例 |
| --- | --- | --- |

| `NameError` | 名前が定義されていない（多くは打ち間違い） | `print(totl)` |

| `TypeError` | その型では操作できない | `"5" + 3` |

| `ValueError` | 型は正しいが、値が不正 | `int("百")` |

| `KeyError` | 辞書に存在しないキーがある | `d["missing"]` |

| `IndexError` | インデックスがリストの範囲外 | `[1, 2][5]` |

| `AttributeError` | オブジェクトにそのメソッドや属性がない | `[].push(1)` |

| `ZeroDivisionError` | ゼロ除算 | `1 / 0` |

| `FileNotFoundError` | ファイルが見つからない | `open("nope.txt")` |

`SyntaxError`（とその特殊な例 `IndentationError`）は別物です。コードを解析する段階、実行前に発生するため、同じファイルの `try` では捕まえられません。

### 例外の階層

例外はクラスであり、階層構造を持っています。頂点は `BaseException` です。そこから `Exception`（ふつうのエラーのほとんど）と、`KeyboardInterrupt`、`SystemExit`、`GeneratorExit` などのシステム用の例外が派生します。

```
BaseException
 ├── KeyboardInterrupt
 ├── SystemExit
 └── Exception
      ├── ArithmeticError
      │    └── ZeroDivisionError
      ├── LookupError
      │    ├── KeyError
      │    └── IndexError
      ├── OSError
      │    └── FileNotFoundError
      ├── TypeError
      └── ValueError
```

階層は、捕まえるときに重要です。`except LookupError` は `KeyError` と `IndexError` の両方を捕まえます。また、`KeyboardInterrupt` はコードのエラーではありません。ユーザーが Ctrl+C を押したことをPythonが知らせる仕組みです。`except Exception` でプログラムの中断を妨げないように、`Exception` を継承していません。

## 例外処理：try/except/else/finally

失敗するかもしれないコードは、`try` ブロックに入れます。`except` ブロックでは、捕まえる例外の型と、それに対する処理を書きます。Pythonで例外を処理するのは、まさにこの `except` です。

```
def parse_amount(raw):
    try:
        value = float(raw)
    except ValueError as e:
        print(f"行をスキップします: {e}")
        return None
    else:
        return value          # 例外が起きなかった場合のみ実行
    finally:
        print("解析完了")      # 必ず実行

print(parse_amount("250"))
```

この出力は、まず「解析完了」、次に `250.0` の順です。`else` の中の `return` は値を覚えておき、Pythonは必ず `finally` を実行してから、結果を返します。そのため `finally` は、ファイルを閉じる、接続を解放するといった後片付けに信頼して使えます。

`finally` の中に独自の `return` を書くと、前の `return` を上書きし、例外まで飲み込んでしまいます。Python 3.14以降では、このようなコードに `SyntaxWarning` が出ます（PEP 765）。`finally` に `return` は書かないようにしましょう。

### 複数の例外を同時に扱う

`except` は、型ごとに複数書けます。また、型をタプルにまとめることもできます。

```
try:
    amount = int(data["amount"])
except KeyError:
    amount = 0
except (ValueError, TypeError) as e:
    print("金額の形式が不正です:", e)
    amount = 0
```

ブロックは上から順に調べられ、最初に一致したものだけが実行されます。そのため、より具体的な型を上に、一般的な型を下に書きます。Python 3.14（PEP 758）からは、型を括弧なしで並べられます（`except ValueError, TypeError:`）。ただし `as` を使う場合は、今まで通り括弧が必要です。

### エラーのテキストと型を調べる

`as e` で例外オブジェクトを受け取れます。`str(e)` はメッセージを、`type(e).__name__` は型名を返します。

```
try:
    int("百")
except ValueError as e:
    print(type(e).__name__)   # ValueError
    print(e)                  # invalid literal for int() with base 10: '百'
```

## tracebackモジュール：完全なエラーテキスト

例外は捕まえたいけれど、トレースバック全体はログやレポートに残したい、という場合があります。その目的には `traceback` モジュールを使います。

```
import traceback

try:
    1 / 0
except ZeroDivisionError:
    traceback.print_exc()             # トレースバックを標準エラー出力に表示
    text = traceback.format_exc()     # 同じ内容を文字列として取得
```

`logging` モジュールでログを書いているなら、`except` の中で `logging.exception("計算に失敗しました")` を呼ぶのが簡単です。トレースバックは自動的にメッセージに追加されます。

## Pythonのraise：例外を発生させる

`raise` 文は、例外を手動で送出します。関数は、そのデータでは作業を実行できないことを、この方法で伝えます。

```
def add_expense(amount):
    if amount < 0:
        raise ValueError(f"金額は負の値にできません: {amount}")
    ...

add_expense(-5)
# ValueError: 金額は負の値にできません: -5
```

Pythonの `raise` は関数ではなく文なので、`raise` の後に括弧は付けません。括弧の中に渡すのは、例外クラスのコンストラクタの引数です。型は意味に応じて選びます。値が不正なら `ValueError`、型が不正なら `TypeError` です。

### 引数なしのraiseとraise from

`except` の中で引数なしの `raise` を書くと、現在の例外が何も変えられずにそのまま上位へ渡されます。ログだけ書いて、処理は呼び出し側に任せたい場合に使います。

ある例外を別の例外に置き換えたいときは、`raise ... from e` を使います。元のエラーがチェーンとして保持され、トレースバックには両方が表示されます。

```
def load_config(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError as e:
        raise RuntimeError(f"設定ファイルがありません: {path}") from e
```

### add_note：エラーに文脈を追加する

Python 3.11（PEP 678）からは、`add_note()` メソッドで例外に注釈を追加し、そのまま再送出できます。例外の型は変わらず、注釈はメッセージの下に表示されます。

```
def parse(raw, line_no):
    try:
        return float(raw)
    except ValueError as e:
        e.add_note(f"ファイル expenses.csv、{line_no}行目")
        raise

parse("百", 17)
# ValueError: could not convert string to float: '百'
# ファイル expenses.csv、17行目
```

## Pythonで独自の例外を作る

独自の例外は、業務ドメインのエラーを表すために使います。例えば、予算超過、支払い済みの注文、ブロックされたユーザーなどです。`Exception` を継承したクラスを作るだけで十分です。

```
class BudgetExceeded(Exception):
    def __init__(self, category, limit, spent):
        super().__init__(f"{category}: 上限 {limit}、支出 {spent}")
        self.category = category
        self.limit = limit
        self.spent = spent

def check_budget(category, limit, spent):
    if spent > limit:
        raise BudgetExceeded(category, limit, spent)

try:
    check_budget("食費", 10000, 12500)
except BudgetExceeded as e:
    print("警告:", e)   # 警告: 食費: 上限 10000、支出 12500
```

これで呼び出し側のコードは、予算超過だけを捕まえられ、打ち間違いなどと混同しなくなります。大きなプロジェクトでは、共通の基底クラス（`class TrackerError(Exception)`）を作り、独自の例外をすべてそこから継承させると便利です。そうすれば `except TrackerError` 1つで、すべてを捕まえられます。継承元は `BaseException` ではなく、必ず `Exception` にしてください。

## 例外グループとexcept*

エラーが同時に複数発生することもあります。例えば、ファイル全体を検査するとき、あるいは `asyncio.TaskGroup` で並列に実行したタスクの場合です。Python 3.11以降では、それらを `ExceptionGroup` にまとめ、`except*` で型ごとに処理できます。

```
def check_all(rows):
    errors = []
    for r in rows:
        try:
            float(r)
        except ValueError as e:
            errors.append(e)
    if errors:
        raise ExceptionGroup("ファイルのエラー", errors)

try:
    check_all(["1", "a", "b"])
except* ValueError as eg:
    print(f"不正な行の数: {len(eg.exceptions)}")   # 不正な行の数: 2
```

## try exceptですべてのエラーを捕まえてはいけない理由

最も有害な習慣は、何でも捕まえてしまうことです。`except: pass` と書くと、プログラムは「クラッシュしなくなります」が、実際には問題を知らせなくなるだけです。引数なしの `except` は `KeyboardInterrupt` や `SystemExit` まで捕まえるため、Ctrl+C でプログラムを止められなくなります。`except Exception: pass` は少しましですが、それでも名前の打ち間違いや論理エラーを黙って飲み込んでしまいます。

Pythonで例外を正しく処理するには、次のようにします。

- 処理できる型だけを捕まえます。すべてではなく、例えば `except ValueError` です。
- `try` のブロックは短くします。その型で失敗しそうな行だけを入れましょう。
- プログラムの最上位で広く捕まえる（`except Exception`）場合は、必ずトレースバックをログに残します。
- 単純な分岐に例外を使わないようにします。簡単な場合は `try/except KeyError` より `dict.get()` で十分です。
- リソースには `with` を使います。エラーが起きても、ファイルを確実に閉じてくれます。

## まとめ

Pythonの例外は、エラーを知らせるための仕組みであり、隠すためのものではありません。tracebackは下から上へ読み、`try/except` では具体的な型だけを捕まえましょう。`else` は「うまくいった場合」のコードに、`finally` は後片付けに使います。Pythonの `raise` で自分からエラーを発生させられ、`raise from` と `add_note` で文脈を残せます。独自の例外は `Exception` から継承して作ります。講座では、このテーマを実践で扱っています。家計簿ツールが、CSVの1行の不正な値で止まらないようにし、予算超過を独自の例外で表現します。

## FAQ

**エラーと例外の違いは何ですか？**

日常会話では同じ意味で使われます。厳密には、SyntaxError はコードの解析時に起きるもので、それ以外のエラーは実行時に例外オブジェクトとして発生し、捕まえられるものです。

**例外が発生した場所から、処理を続けることはできますか？**

いいえ。捕まえた後は、エラーが起きた行ではなく、try/except ブロックの後から実行が続きます。ファイルの各行を処理したいなら、try をループの中に置きましょう。

**except の中で例外が起きたらどうなりますか？**

その新しい例外が上位へ送られ、元の例外は __context__ 属性に保存されます。トレースバックには「During handling of the above exception, another exception occurred」という文が表示されます。

**テストで、関数が例外を発生させることを確かめるにはどうしますか？**

pytest では with pytest.raises(ValueError): を使い、unittest では self.assertRaises(ValueError) を使います。ブロックの中で指定した型の例外が起きた場合にのみ、テストが通ります。

**assert と raise の違いは何ですか？**

assert 条件, "メッセージ" は、条件が偽のときに AssertionError を送出します。開発中のチェック用の仕組みです。-O オプションで起動すると assert は無効になるため、ユーザーの入力の検証は if と raise で行いましょう。


---
Source: Skillaria.Top (https://skillaria.top)
