SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Pythonの新機能を知ろう!

Python 3.10~3.12の魅力:入門者も使いやすいf-string、ExceptionGroup、進化したエラーメッセージの機能を解説

Pythonの新機能を知ろう! 第4回

ExceptionGroupとTaskGroupのサポートで例外処理を効率的に扱う[3.11]

 Python 3.11では、複数の例外をまとめるExceptionGroupが導入されています。これと同時に、複数のタスクの終了と例外の発生を管理するTaskGroupが導入されました。ExceptionGroupの目的の一つとして、このTaskGroupにおける例外のハンドリングがあります。

例外をまとめるExceptionGroup

 まずはExceptionGroupを紹介します。ExceptionGroupはその名の通り例外グループで、複数の例外オブジェクトを1個の例外グループにまとめます。例外オブジェクトの他に、例外グループをまとめるという入れ子も可能です。

exceptiongroup.py
exceptions = [UserException1("ユーザ例外1"), UserException2("ユーザ例外2")]
raise ExceptionGroup("2つのユーザ例外です。", exceptions)

 実行すると以下のように例外が発生し、ExceptionGroupオブジェクトと2つのサブ例外オブジェクトが表示されます。

  + Exception Group Traceback (most recent call last):
  |   File "exceptiongroup.py", line 11, in <module>
  |     raise ExceptionGroup("2つのユーザ例外です。", exceptions)
  | ExceptionGroup: 2つのユーザ例外です。 (2 sub-exceptions)
  +-+---------------- 1 ----------------
    | UserException1: ユーザ例外1
    +---------------- 2 ----------------
    | UserException2: ユーザ例外2
    +------------------------------------

 このように、ExceptionGroupの動きは非常にシンプルですが、用途は以下のような局面での複数の例外のハンドリングという幅広いものです。

  • 後述する並列処理で発生する全ての例外
  • socket.create_connection関数などで処理の再試行で発生する例外
  • ユーザーコールバック関数やファイナライザで発生するそれぞれの例外
  • Hypothesisライブラリなどで複雑な計算処理の過程で発生する全ての例外

複数の例外をキャッチするtry~except*ブロック

 ExceptionGroup自体も例外オブジェクトであり、try~exceptブロックで捕捉できます。ただ、例外グループのいずれかの例外が発生したということしか分からず、個別の例外の種類はisinstanceメソッドで逐一判別したり、subgroupメソッドで個別の例外を取り出したりする必要があるなど、その利用は若干煩雑です。そこで、個別の例外を直接捕捉できるtry~except*ブロック(「*」が付いている)がPython 3.11で同時に導入されました。これを使うと、上記の例外は以下のリストのように捕捉できます。

exceptiongroup.py
try:
    exceptions = [UserException1("ユーザ例外1"), UserException2("ユーザ例外2")]
    raise ExceptionGroup("2つのユーザ例外です。", exceptions)

except* UserException1 as e:
    print(repr(e))

except* UserException2 as e:
    print(repr(e))

# 実行結果:
ExceptionGroup('2つのユーザ例外です。', [UserException1('ユーザ例外1')])
ExceptionGroup('2つのユーザ例外です。', [UserException2('ユーザ例外2')])

 ここで注目すべきは、複数の例外に対して複数のexcept*ブロックが実行されている点です。通常のtry~exceptブロックでは、最初に捕捉されたexceptブロックのみが実行されるので、大きな違いです。さらに、except*ブロックにおいて例外を変数eで受け取っていますが、それはExceptionGroupオブジェクトであり個別の例外オブジェクトではありません。このExceptionGroupオブジェクトには、except*ブロックで捕捉された例外オブジェクトのみが含まれており、例外の内容に基づく処理を実行できます。なお、一つのtryブロックにexceptブロックとexcept*ブロックを混在させることはできません。

タスクをまとめるTaskGroup

 前述したように、ExceptionGroupは非同期で動作するタスクにおいて発生する例外を扱うときに特に有用です。同期処理では発生する例外も常に1つなので特別な考慮は不要ですが、非同期処理では複数のタスクで個別に例外が発生する可能性があるので、それらを全て捕捉しようとすると特別な仕組みが必要です。そこでPython 3.11で導入されたのがTaskGroupです。

 TaskGroupは非同期コンテキストマネジャーと呼ばれ、そのcreate_taskメソッドで登録された全てのタスクの実行の終了と例外の発生を管理します。例外がいずれかのタスクで発生すると、それはExceptionGroupオブジェクトにまとめられて送出され、他の実行中のタスクは終了させられます。以下のリストは、ランダムに例外を発生させる非同期関数を3つTaskGroupに登録し、例外の発生がなくなるまで繰り返す例です。

taskgroup.py
# 1~6の乱数を発生させ、1~3なら例外、4~6なら関数の戻り値とする
async def coroutine():
    value = random.randint(1, 6)
    match(value):
        case 1:
            raise UserException1("ユーザ例外1")
        case 2:
            raise UserException2("ユーザ例外2")
        case 3:
            raise UserException3("ユーザ例外3")
        case _:
            return value

# 呼び出し回数を表示してcoroutineタスクを3個実行。それぞれの戻り値の総計を返す
async def call(count):
    print(f"{count} 回目の呼び出しです。")
    async with asyncio.TaskGroup() as tg:
        task1 = tg.create_task(coroutine())
        task2 = tg.create_task(coroutine())
        task3 = tg.create_task(coroutine())
    return task1.result() + task2.result() + task3.result()

# callタスクを無限に呼び出す。例外が運良く発生しなければ終了
async def main():
    count = 0
    while True:
        count = count + 1
        try:
            print(f"{await call(count)} が返りました。")
            break
        except* (UserException1, UserException2, UserException3) as e:
            print(repr(e))
            await asyncio.sleep(1)

asyncio.run(main())

 実行すると、例えば以下のように例外の発生を確認できます(乱数を使っているので実行結果は毎回変化します)。各タスクで発生した例外は、'unhandled errors in a TaskGroup'と名付けられたExceptionGroupオブジェクトにまとめられて送出されるのが分かります。

ExceptionGroup('unhandled errors in a TaskGroup', [UserException2('ユーザ例外2')])
ExceptionGroup('unhandled errors in a TaskGroup', [UserException3('ユーザ例外3'), UserException3('ユーザ例外3')])
2 回目の呼び出しです。
ExceptionGroup('unhandled errors in a TaskGroup', [UserException3('ユーザ例外3')])
…略…
8 回目の呼び出しです。
15 が返りました。

次のページ
エラーメッセージが明確に、初心者もデバッグしやすく

この記事は参考になりましたか?

Pythonの新機能を知ろう!連載記事一覧

もっと読む

この記事の著者

WINGSプロジェクト 山内 直(WINGSプロジェクト ヤマウチ ナオ)

WINGSプロジェクトについて>有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。 2026年時点での登録メンバは約50名で、現在も執筆メンバを募集中。興味のある方は、どしどし応募頂きたい。著書記事多数。 RSS X: @WingsPro_info(公式)、@WingsPro_info/wings(メンバーリスト) Facebook <個人紹介>WINGSプロジェクト所属のテクニカルライター。出版社を経てフリーランスとして独立。ライター、エディター、デベロッパー、講師業に従事。屋号は「たまデジ。」。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

山田 祥寛(ヤマダ ヨシヒロ)

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に「独習シリーズ(Java・C#・Python・PHP・Ruby・JSP&サーブレットなど)」「速習シリーズ(ASP.NET Core・Vue.js・React・TypeScript・ECMAScript、Laravelなど)」「改訂3版JavaScript本格入門」「これからはじめるLaravel実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/18916 2024/01/31 11:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー