Python 3.11で導入されたExceptionGroup例外の使い方7選

ExceptionGroupの徹底解説IoTプログラミング
この記事は約19分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればサンプルコードを活用して機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

●Python ExceptionGroupとは?その特徴と利用

Pythonのバージョン3.11から導入された新しい機能であるExceptionGroupが注目を集めています。

ExceptionGroupは複数の例外を1つのグループとして扱うためのクラスで、例外処理の可読性と保守性の向上に役立ちます。

ExceptionGroupを使うと、関連する複数の例外を1つのグループとして扱えるので、例外の種類ごとに別々の処理を記述する必要がありません。

このように、コードの可読性と保守性が向上するのです。

○従来の例外処理との違い

従来のPythonでは、例外が発生するとその例外が伝播し、対応する最初のexcept句で捕捉されます。

しかし、複数の例外が同時に発生した場合、最初に捕捉された例外以外は無視されてしまいます。

例えば、ファイルの読み込みとデータの処理を行う際に、FileNotFoundErrorとValueErrorが同時に発生したとします。

従来の方法では、最初に発生した例外のみが捕捉され、もう一方の例外は無視されてしまいます。

しかし、ExceptionGroupを使えば、複数の例外をグループ化して一括して処理できます。

これで、発生したすべての例外に適切に対処できるようになります。

○ExceptionGroupが解決する問題

ExceptionGroupは、次のような問題を解決します。

  1. 複数の例外を個別に処理する必要がなくなる
  2. 例外処理のコードがシンプルで読みやすくなる
  3. 関連する例外を見落とすリスクが減る
  4. 並行処理やマルチスレッドプログラミングでのエラーハンドリングが容易になる

特に、非同期プログラミングにおいては、複数のタスクが並行して実行されるため、エラーハンドリングが複雑になりがちです。

ExceptionGroupを活用することで、並行処理で発生する複数の例外を効率的に処理できます。

●ExceptionGroupの基本的な使い方

さて、ExceptionGroupの概要は理解していただけたと思います。

実際に使ってみると、その利便性がよりわかりやすくなるでしょう。

早速、ExceptionGroupの基本的な使い方を見ていきましょう。

○サンプルコード1:シンプルなExceptionGroupの作成

まずは、シンプルなExceptionGroupを作成してみます。

ExceptionGroupクラスのコンストラクタには、グループの名前とExceptionオブジェクトのシーケンスを渡します。

from exceptiongroup import ExceptionGroup

def func1():
    raise ValueError("値が不正です")

def func2():
    raise TypeError("型が違います")

try:
    func1()
    func2()
except* ExceptionGroup as e:
    print(f"Caught {e}")

実行結果

Caught ExceptionGroup(ValueError('値が不正です'), TypeError('型が違います'))

func1()とfunc2()の両方で例外が発生しています。

ExceptionGroupを使うことで、それらの例外を一括して捕捉できました。

複数の例外を個別にexceptブロックで処理する必要がないのは、コードの可読性を高めてくれます。

○サンプルコード2:ネストされたExceptionGroupの扱い

次に、ネストされたExceptionGroupを扱う方法を見ていきましょう。

ExceptionGroupは階層的に構成することができます。

from exceptiongroup import ExceptionGroup

def func1():
    try:
        raise ValueError("値が不正です")
    except ValueError as e:
        raise ExceptionGroup("func1のエラー", [e])

def func2():
    try:
        raise TypeError("型が違います")
    except TypeError as e:
        raise ExceptionGroup("func2のエラー", [e])

try:
    func1()
    func2()
except* ExceptionGroup as e:
    print(f"Caught {e}")
    for exc in e.exceptions:
        if isinstance(exc, ExceptionGroup):
            print(f"  Nested: {exc}")
        else:
            print(f"  {type(exc).__name__}: {exc}")

実行結果

Caught ExceptionGroup(ExceptionGroup('func1のエラー', ValueError('値が不正です')), ExceptionGroup('func2のエラー', TypeError('型が違います')))
  Nested: ExceptionGroup('func1のエラー', ValueError('値が不正です'))
  ValueError: 値が不正です
  Nested: ExceptionGroup('func2のエラー', TypeError('型が違います'))
  TypeError: 型が違います

func1()とfunc2()内でExceptionGroupを作成し、元の例外をラップしています。

外側のtry-exceptブロックでは、ネストされたExceptionGroupを含むExceptionGroupを捕捉しています。

捕捉したExceptionGroupの中身を確認するには、例外オブジェクトのexceptions属性を使います。

exceptionsには、グループ内の各例外オブジェクトが格納されています。

isinstance()を使って、ネストされたExceptionGroupかどうかを判定しながら、適切にエラーメッセージを表示しています。

●ExceptionGroupと非同期処理

ExceptionGroupは同期的な処理だけでなく、非同期処理でも大活躍します。

非同期プログラミングでは、複数のタスクが並行して実行されるため、エラーハンドリングが複雑になりがちですよね。

しかし、ExceptionGroupを使えば、非同期処理で発生する複数の例外を簡潔に処理できるんです。

○サンプルコード3:asyncioとExceptionGroupの組み合わせ

早速、ExceptionGroupとasyncioを組み合わせた例を見ていきましょう。

import asyncio
from exceptiongroup import ExceptionGroup

async def task1():
    await asyncio.sleep(1)
    raise ValueError("task1でエラー")

async def task2():
    await asyncio.sleep(2)
    raise TypeError("task2でエラー")

async def main():
    try:
        await asyncio.gather(task1(), task2())
    except* ExceptionGroup as e:
        print(f"Caught {e}")
        for exc in e.exceptions:
            print(f"  {type(exc).__name__}: {exc}")

asyncio.run(main())

実行結果

Caught ExceptionGroup(ValueError('task1でエラー'), TypeError('task2でエラー'))
  ValueError: task1でエラー
  TypeError: task2でエラー

asyncio.gather()を使って、task1()とtask2()を並行して実行しています。

各タスクでは、わざと異なる種類の例外を発生させています。

try-exceptブロックでExceptionGroupを捕捉し、グループ内の各例外を順番に処理しています。

非同期タスクで発生したすべての例外を一括して扱えるのは、とても便利ですよね。

○サンプルコード4:TaskGroupを使った並行処理のエラーハンドリング

Python 3.11では、asyncioにTaskGroupというクラスが追加されました。

TaskGroupを使うと、複数のタスクを簡単にグループ化でき、エラーハンドリングも簡潔に書けます。

import asyncio
from exceptiongroup import ExceptionGroup

async def task1():
    await asyncio.sleep(1)
    raise ValueError("task1でエラー")

async def task2():
    await asyncio.sleep(2)
    raise TypeError("task2でエラー")

async def main():
    async with asyncio.TaskGroup() as tg:
        tg.create_task(task1())
        tg.create_task(task2())

asyncio.run(main())

実行結果:

Traceback (most recent call last):
  ...
  File "...", line 6, in task1
    raise ValueError("task1でエラー")
  File "...", line 10, in task2
    raise TypeError("task2でエラー")
ExceptionGroup: ExceptionGroup(ValueError('task1でエラー'), TypeError('task2でエラー'))

TaskGroupのコンテキストマネージャー内で、create_task()を使ってタスクを作成しています。

タスクで例外が発生すると、自動的にExceptionGroupにまとめられ、伝播します。

明示的にtry-exceptを書かなくても、TaskGroupがエラーハンドリングを肩代わりしてくれるのは嬉しいポイントです。

コードがスッキリして読みやすくなりますね。

●ExceptionGroupの高度な活用法

ExceptionGroupの基本的な使い方は理解できたと思います。

ただ、ExceptionGroupにはもっと高度な活用法が存在します。

それぞれのシチュエーションに合わせて、柔軟にExceptionGroupを使いこなせると、エラーハンドリングの幅がグッと広がります。

○サンプルコード5:except*を使った選択的な例外捕捉

まずは、except*を使った選択的な例外捕捉の方法から見ていきましょう。

except*を使うと、ExceptionGroupから特定の例外だけを取り出して処理できます。

from exceptiongroup import ExceptionGroup

def func1():
    raise ValueError("値が不正です")

def func2():
    raise TypeError("型が違います")

try:
    func1()
    func2()
except* ValueError as e:
    print(f"ValueErrorをキャッチ: {e}")
except* TypeError as e:
    print(f"TypeErrorをキャッチ: {e}")
except* Exception as e:
    print(f"その他の例外をキャッチ: {e}")

実行結果

ValueErrorをキャッチ: ExceptionGroup(ValueError('値が不正です'))
TypeErrorをキャッチ: ExceptionGroup(TypeError('型が違います'))

例外の種類ごとにexcept*ブロックを用意することで、ExceptionGroup内の特定の例外だけを選択的に処理できました。

例外の種類に応じて、ログの出力やリソースの解放などの処理を個別に行えるので、とても便利ですよね。

○サンプルコード6:ExceptionGroupのフィルタリングとグループ化

ExceptionGroupの中から、条件に合う例外だけを抽出したい場合もあると思います。

そんなときは、ExceptionGroup.subgroup()メソッドを使ってフィルタリングできます。

from exceptiongroup import ExceptionGroup

def is_value_error(exc):
    return isinstance(exc, ValueError)

try:
    raise ExceptionGroup("例外グループ",
        [ValueError("値が不正です"), TypeError("型が違います"), ValueError("もう一つのValueError")])
except* ExceptionGroup as e:
    value_error_group = e.subgroup(is_value_error)
    print(f"ValueErrorのグループ: {value_error_group}")

    other_group = e.subgroup(lambda exc: not is_value_error(exc))
    print(f"その他の例外グループ: {other_group}")

実行結果

ValueErrorのグループ: ExceptionGroup('例外グループ', ValueError('値が不正です'), ValueError('もう一つのValueError'))
その他の例外グループ: ExceptionGroup('例外グループ', TypeError('型が違います'))

subgroup()メソッドの引数に、例外を判定する関数を渡します。

is_value_error()関数では、例外がValueErrorかどうかを判定しています。

ExceptionGroupをValueErrorとその他の例外に分けて、新しいExceptionGroupを作成しました。

関連する例外をグループ化することで、例外の関連性が明確になり、エラー処理のロジックがシンプルになります。

○サンプルコード7:カスタムExceptionGroupの作成と活用

ExceptionGroupは組み込みのクラスですが、カスタムExceptionGroupを作ることもできます。

プロジェクト固有の例外グループを定義することで、エラー処理をより semantic に行えます。

from exceptiongroup import ExceptionGroup

class CustomExceptionGroup(ExceptionGroup):
    def __init__(self, message, exceptions):
        super().__init__(message, exceptions)
        self.log_error()

    def log_error(self):
        print(f"エラーログ: {self}")

try:
    raise CustomExceptionGroup("カスタム例外グループ",
        [ValueError("値が不正です"), TypeError("型が違います")])
except* CustomExceptionGroup as e:
    print(f"カスタムExceptionGroupをキャッチ: {e}")

実行結果

エラーログ: CustomExceptionGroup('カスタム例外グループ', ValueError('値が不正です'), TypeError('型が違います'))
カスタムExceptionGroupをキャッチ: CustomExceptionGroup('カスタム例外グループ', ValueError('値が不正です'), TypeError('型が違います'))

CustomExceptionGroupクラスを定義し、ExceptionGroupを継承しています。

__init()__メソッドをオーバーライドし、例外が発生した際に自動的にエラーログを出力するようにしました。

カスタムExceptionGroupを使うことで、例外グループに独自の振る舞いを持たせられます。

プロジェクトに適した例外処理の仕組みを構築できるので、コードの可読性と保守性が向上するでしょう。

●ExceptionGroup使用時の注意点とベストプラクティス

ExceptionGroupを使えば、複数の例外を効率的に処理できるようになります。

でも、ExceptionGroupを使う際には、いくつか注意点があるんです。

適切に使わないと、かえってコードが複雑になったり、パフォーマンスに影響が出たりする可能性があります。

○バージョン互換性の考慮

ExceptionGroupはPython 3.11で導入された新しい機能です。

Python 3.11より古いバージョンでExceptionGroupを使おうとすると、エラーが発生してしまいます。

例えば、Python 3.10以前のバージョンで次のようなコードを実行すると、ModuleNotFoundErrorが発生します。

from exceptiongroup import ExceptionGroup

try:
    # 例外を発生させる処理
    pass
except* ExceptionGroup as e:
    # 例外の処理
    pass

実行結果(Python 3.10以前)

Traceback (most recent call last):
  File "example.py", line 1, in <module>
    from exceptiongroup import ExceptionGroup
ModuleNotFoundError: No module named 'exceptiongroup'

プロジェクトのPythonバージョンを3.11以降にアップグレードできない場合は、ExceptionGroupを使わずに従来の例外処理を行う必要があります。

バージョンの互換性を考慮して、適切に例外処理のコードを書きましょう。

○パフォーマンスへの影響

ExceptionGroupを使うと、例外処理のコードがシンプルになる反面、パフォーマンスへの影響を考慮する必要があります。

特に、大量の例外を含むExceptionGroupを処理する場合は、注意が必要です。

次のコードは、1000個の例外を含むExceptionGroupを処理する例です。

from exceptiongroup import ExceptionGroup

try:
    exceptions = [ValueError(f"例外{i}") for i in range(1000)]
    raise ExceptionGroup("大量の例外", exceptions)
except* ExceptionGroup as e:
    for exc in e.exceptions:
        print(f"例外をキャッチ: {exc}")

実行結果(一部省略)

例外をキャッチ: ValueError('例外0')
例外をキャッチ: ValueError('例外1')
...
例外をキャッチ: ValueError('例外998')
例外をキャッチ: ValueError('例外999')

大量の例外を含むExceptionGroupを処理すると、例外の数に応じてループ処理の回数が増え、パフォーマンスが低下する可能性があります。

パフォーマンスを改善するには、例外の数を減らすことが有効です。

関連する例外をグループ化したり、不要な例外を除外したりするなどの工夫をしましょう。

また、例外処理のコードを最適化し、無駄な処理を減らすことも大切です。

●よくあるエラーと対処法

ExceptionGroupを使っていると、時々エラーに遭遇することがあるかもしれません。

でも、焦る必要はありません。

よくあるエラーとその対処法を知っておけば、スムーズにトラブルシューティングができます。

○ModuleNotFoundError: No module named ‘exceptiongroup’

Python 3.11未満のバージョンでExceptionGroupを使おうとすると、ModuleNotFoundErrorが発生します。

from exceptiongroup import ExceptionGroup

try:
    # 例外を発生させる処理
    pass
except* ExceptionGroup as e:
    # 例外の処理
    pass

実行結果(Python 3.10以前)

Traceback (most recent call last):
  File "example.py", line 1, in <module>
    from exceptiongroup import ExceptionGroup
ModuleNotFoundError: No module named 'exceptiongroup'

対処法は簡単です。Python 3.11以降のバージョンにアップグレードすることです。

Python 3.11では、ExceptionGroupがデフォルトで利用可能になっています。

Pythonのバージョンアップができない場合は、従来の例外処理の方法を使う必要があります。

バージョンの互換性を考慮して、適切にコードを書き換えましょう。

○TypeError: catch() argument must be a type, a tuple of types, or an exception group

except*構文を使う際に、引数の型が正しくない場合に発生するエラーです。

try:
    # 例外を発生させる処理
    pass
except* "ValueError" as e:  # 型が正しくない
    # 例外の処理
    pass

実行結果

Traceback (most recent call last):
  File "example.py", line 4, in <module>
    except* "ValueError" as e:
  File "<string>", line 4
    except* "ValueError" as e:
            ^^^^^^^^^^^^^^^^^
TypeError: catch() argument must be a type, a tuple of types, or an exception group

対処法は、except*の引数に正しい型を指定することです。

引数には、例外の型、例外の型のタプル、またはExceptionGroupを指定します。

try:
    # 例外を発生させる処理
    pass
except* ValueError as e:  # 例外の型を指定
    # 例外の処理
    pass

型を正しく指定することで、エラーを回避できます。

コードを書く際は、構文に注意を払いましょう。

まとめ

さて、ExceptionGroupについて詳しく見てきましたが、いかがでしたか?

ExceptionGroupは、Pythonの例外処理を大きく変える機能です。

ぜひ、自分のプロジェクトでExceptionGroupを活用して、より良いコードを書いていきましょう。

最後までお読みいただき、ありがとうございました。