【Python 3.11の新機能】真のエラーバンドリング!ExceptionGroupとexcept*を使いこなす

ALL

こんにちは、Tech Samuraiです!
前回の記事「【Python中級編】もうエラーで止まらない!複数のエラーをまとめて処理する「エラーバンドリング」入門」では、複数のエラーをリストに集め、自作のカスタム例外でラップするという、非常に実用的なエラーハンドリング手法を探検しました。

あの手法は、Pythonの基本的な機能だけで実装できる素晴らしいものでした。しかし、「もし、この『エラーを束ねる』という仕組み自体が、Pythonの標準機能として、もっとエレガントに用意されていたとしたら…?」

実は、**Python 3.11**から、まさにそのための公式な武器、**`ExceptionGroup`**と**`except*`**(except star)という新しい構文が導入されたのです! 今回は、前回のコードをリファクタリングしながら、このモダンで強力なエラーハンドリングの「正解」をマスターしていきましょう。


おさらい:何が課題だったか?

前回の課題を思い出してみましょう。複数のタスクをループで処理する際、一つエラーが発生すると、そこで処理全体が止まってしまうのが問題でした。

# 以前の課題
# ID:3でエラーが出た瞬間に、ID:4以降の処理が実行されずに止まってしまう
for user_id in [2, 3, 4, 5, 6]:
    process_user(user_id)

私たちは、これを解決するために、エラーをリストに溜め込み、最後にカスタム例外でラップして投げる、という手法を自作しました。


Python 3.11からの新しい英雄たち

この問題を、よりエレガントに解決してくれるのが、`ExceptionGroup`と`except*`です。

`ExceptionGroup`:例外を入れるための公式コンテナ

これは、複数の例外オブジェクトを内包できる、Pythonに組み込まれた新しい例外クラスです。私たちが前回自作した`DataProcessingError`の、いわば公式で超高機能なバージョンです。

`except*`:複数の例外を同時にキャッチする新しい構文

これが今回の主役です。`ExceptionGroup`の中から、**特定の型の例外だけをグループとして**キャッチすることができます。

例えるなら… 通常の`except`が「網で魚を一匹ずつ捕まえる」のだとすれば、`except*`は「投網を投げて、アジの群れ、サバの群れを、種類ごとにまとめて捕まえる」ようなイメージです。

try:
    # ここでExceptionGroupが発生する
    raise ExceptionGroup(
        "問題発生",
        [
            ValueError("値が不正です"),
            TypeError("型が違います"),
            ValueError("別の値も不正です"),
        ]
    )
# ExceptionGroupの中から、ValueErrorだけをまとめてキャッチ
except* ValueError as eg:
    print(f"ValueErrorが {len(eg.exceptions)} 件見つかりました。")
    for e in eg.exceptions:
        print(f"- {e}")
# ExceptionGroupの中から、TypeErrorだけをまとめてキャッチ
except* TypeError as eg:
    print(f"TypeErrorが {len(eg.exceptions)} 件見つかりました。")

このように、エラーの種類に応じて、後処理を分岐させることができるのです。


実践:前回のコードをリファクタリングする

それでは、前回の「エラー収集ロジック」を、この新しい構文を使って書き直してみましょう。

def process_user(user_id):
    if user_id % 2 == 0:
        print(f"ユーザーID: {user_id} の処理が成功しました。")
    else:
        raise ValueError(f"ユーザーID: {user_id} は無効なIDです。")

user_ids = [2, 3, 4, 5, 6]
errors = []

# --- エラーの収集部分は同じ ---
for user_id in user_ids:
    try:
        process_user(user_id)
    except ValueError as e:
        errors.append(e)

# --- ここからが新しい! ---
# 1. もしエラーがあれば、公式のExceptionGroupとして投げる
if errors:
    raise ExceptionGroup("いくつかのユーザー処理でエラーが発生しました", errors)

print("\n全ての処理が完了しました。")

そして、このコードを呼び出す側は、`except*`を使って以下のようにスマートにエラーを処理できます。

try:
    # (上記のエラー収集コードをここに記述)
    # ...

# 2. ExceptionGroupの中から、ValueErrorのグループだけをキャッチ
except* ValueError as eg:
    print("\n--- 最終エラー報告 ---")
    print(f"合計 {len(eg.exceptions)} 件の無効なIDエラーが検出されました。")
    print("▼ 詳細:")
    for error in eg.exceptions:
        print(f"  - {error}")
# もし他の種類のエラー(例: TypeError)があれば、ここに追加できる
# except* TypeError as eg:
#     ...

実行結果:

ユーザーID: 2 の処理が成功しました。
ユーザーID: 4 の処理が成功しました。
ユーザーID: 6 の処理が成功しました。

--- 最終エラー報告 ---
合計 2 件の無効なIDエラーが検出されました。
▼ 詳細:
  - ユーザーID: 3 は無効なIDです。
  - ユーザーID: 5 は無効なIDです。

カスタム例外を自作した前回と、最終的な結果は同じです。しかし、Pythonに組み込まれた標準の仕組みを使うことで、より意図が明確で、他の開発者にも理解しやすいコードになりました。


まとめ

今回は、Python 3.11から導入された、モダンなエラーハンドリング機能について探検しました。

  • 複数の例外を束ねるための公式な器、ExceptionGroup
  • その中から、特定の型の例外群をまとめてキャッチする、新しい構文except*

特に、複数のネットワーク通信を並行して行う**非同期処理**などでは、複数の処理が同時に失敗することがよくあります。そのような場面で、この`ExceptionGroup`は真価を発揮します。

エラーハンドリングは、プログラムの堅牢性を決める重要な要素です。Pythonの進化に合わせて、私たちのコードも常にアップデートしていきましょう!

さて、少し専門的なエラー処理の世界を探検しました。次回は、がらりとテーマを変えて、ドローンを飛ばすまでの流れをシミュレーションしていきます!

コメント

タイトルとURLをコピーしました