複数のタスクから独立に例外が発生したらどうすればよいだろう。これをスマートに処理するためにPython 3.11に追加されたExceptionGroupやTaskGroupなどを紹介する。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
前回はPython 3.11に組み込まれたFaster CPythonプロジェクトの成果について見た。今回はPython 3.11で新たに追加された例外クラス「ExceptionGroup」とこれを取り扱うためのexcept*節、ExceptionGroup例外と組で使うことになるであろうTaskGroupクラスについて見ていく。
ExceptionGroup例外とexcept*節はPEP 654で提案されたものだ。簡単にまとめるとExceptionGroup例外とは、無関係な例外を複数まとめて処理するための機構だ。そして、まとめられた複数の例外を個別に処理するために追加されたのがexcept*節となる。
簡単な例を以下に示す。
raise ExceptionGroup(
'exception group',
[
TypeError('TypeError in ExceptionGroup'),
ValueError('ValueError in ExceptionGroup')
]
)
TypeError例外とValueError例外をExceptionGroup例外にまとめて送出している。このように、ExceptionGroup例外のインスタンスを生成する際には、例外についてのメッセージとそこに含める例外を要素とするシーケンス(リストなど)を与える。これを実行すると、その結果は次のようになる。
>>> raise ExceptionGroup(
... 'exception group',
... [
... TypeError('TypeError in ExceptionGroup'),
... ValueError('ValueError in ExceptionGroup')
... ]
... )
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 1, in <module>
| ExceptionGroup: exception group (2 sub-exceptions)
+-+---------------- 1 ----------------
| TypeError: TypeError in ExceptionGroup
+---------------- 2 ----------------
| ValueError: ValueError in ExceptionGroup
+------------------------------------
先のコードは例外を送出するだけだったので、その結果は例外についてのトレースバック情報の表示となる。最初に概要が、その下にインデント付きでExceptionGroupでラップした2つの例外が並んでいることが分かる。
なお、シーケンスに格納する例外の中にさらにExceptionGroup例外を含めることも可能だ。
raise ExceptionGroup(
'exception group',
[
TypeError('TypeError in ExceptionGroup'),
ValueError('ValueError in ExceptionGroup'),
ExceptionGroup(
'another exception group',
[RuntimeError('RuntimeError in another ExceptionGroup')]
)
]
)
これを実行すると次のようになる。
>>> raise ExceptionGroup(
... 'exception group',
... [
... TypeError('TypeError in ExceptionGroup'),
... ValueError('ValueError in ExceptionGroup'),
... ExceptionGroup(
... 'another exception group',
... [RuntimeError('RuntimeError in another ExceptionGroup')]
... )
... ]
... )
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 1, in <module>
| ExceptionGroup: exception group (3 sub-exceptions)
+-+---------------- 1 ----------------
| TypeError: TypeError in ExceptionGroup
+---------------- 2 ----------------
| ValueError: ValueError in ExceptionGroup
+---------------- 3 ----------------
| ExceptionGroup: another exception group (1 sub-exception)
+-+---------------- 1 ----------------
| RuntimeError: RuntimeError in another ExceptionGroup
+------------------------------------
ネストしたExceptionGroup例外の内容がさらにインデントを付けられて表示されていることに注意しよう。
ExceptionGroup例外を処理するには大きく分けて2つのやり方がある。
1つはExceptionGroup例外にまとめられている個々の例外をexcept*節で処理する方法だ。基本的には以下のような形になる。
try:
# ExceptionGroup例外を送出するコード
except* TypeError as e:
# ExceptionGroup例外に格納されているTypeError例外を処理
except* ValueError as e:
# ExceptionGroup例外に格納されているValueError例外を処理
もう1つはExceptionGroup例外全体をexcept節でまとめて捕捉する方法だ。こちらは通常のtry〜except文と同様だ。
try:
# ExceptionGroup例外を送出するコード
except ExceptionGroup as eg:
# ExceptionGroup例外に格納されている例外を個別に取り出して処理する
以下ではこれらについて簡単に見ていく。
以下は本稿の冒頭でお目にかけたExceptionGroup例外を送出するコードをtry〜except*節で処理するコードだ。
try:
raise ExceptionGroup(
'exception group',
[
TypeError('TypeError in ExceptionGroup'),
ValueError('ValueError in ExceptionGroup')
]
)
except* TypeError as e:
print(type(e))
print(repr(e))
except* ValueError as e:
print(repr(e))
ここではTypeErrorとValueErrorの2つの例外がExceptionGroup例外にまとめて送出されており、それらを2つのexcept*節で処理する。通常のtry〜except文では複数のexcept節があっても何かの例外を処理するときにはいずれかのexcept節のみが実行される。しかし、except*節ではExceptionGroupに格納されている例外を(それにマッチするexcept*節があれば)全て処理できるのが大きな違いである。
実際に実行すると次のようになる。
>>> try:
... raise ExceptionGroup(
... 'exception group',
... [
... TypeError('TypeError in ExceptionGroup'),
... ValueError('ValueError in ExceptionGroup')
... ]
... )
... except* TypeError as e:
... print(type(e))
... print(repr(e))
... except* ValueError as e:
... print(repr(e))
... print(e.exceptions)
...
<class 'ExceptionGroup'>
ExceptionGroup('exception group', [TypeError('TypeError in ExceptionGroup')])
ExceptionGroup('exception group', [ValueError('ValueError in ExceptionGroup')])
(ValueError('ValueError in ExceptionGroup'),)
print関数による4つの出力(最後の4行)に注目してほしい。最初の行は、捕捉したTypeErrorを代入した変数eの型を表示したものだが、TypeErrorではなくExceptionGroupになっている。ExceptionGroup例外をexcept*節で捕捉したときには、それを代入した変数の型は常にExceptionGroupになる。実際の例外を取り出すには、そのexceptions属性にアクセスする(後述)。
その下の2行は変数eをrepr関数で文字列化した結果だ。これを見ると、最初のexcept*節でTypeError例外が、次のexcept*節でValueError例外が捕捉されているのが分かる。
最後の行が上述したExceptionGroupオブジェクトのexceptions属性を表示したものだ。ここではExceptionGroup例外に格納されていたTypeError例外を表すオブジェクトがタプルの要素となっていることが分かる。例外発生時の情報を取り出して、何らかの処理をするのであれば、ここからそれらにアクセスできるだろう。
なお、ExceptionGroup例外をexcept*節で処理することは許されていない。ExceptionGroup例外を捕捉して処理するにはexcept節を使用する必要がある。
次にexcept節でExceptionGroup例外の全体を捕捉して処理する例を見てみよう。ここでも、例外を送出するコードは同じものとする。
try:
raise ExceptionGroup(
'exception group',
[
TypeError('TypeError in ExceptionGroup'),
ValueError('ValueError in ExceptionGroup')
]
)
except ExceptionGroup as eg:
print(eg.exceptions)
実行結果を以下に示す。
>>> try:
... raise ExceptionGroup(
... 'exception group',
... [
... TypeError('TypeError in ExceptionGroup'),
... ValueError('ValueError in ExceptionGroup')
... ]
... )
... except ExceptionGroup as eg:
... print(eg.exceptions)
...
(TypeError('TypeError in ExceptionGroup'), ValueError('ValueError in ExceptionGroup'))
先ほどと同じく、exceptions属性の内容を表示してみると、ExceptionGroup例外に格納されている2つの例外がタプルに格納されていることが分かる。
ではこれらの例外を個別に処理するにはどうするかというと、例えば、以下のようなコードが考えられる。
try:
raise ExceptionGroup(
'exception group',
[
TypeError('TypeError in ExceptionGroup'),
ValueError('ValueError in ExceptionGroup')
]
)
except ExceptionGroup as eg:
print(eg.exceptions)
typeerr_group, rest = eg.split(lambda e: isinstance(e, TypeError))
valerr_group, rest = rest.split(lambda e: isinstance(e, ValueError))
if typeerr_group:
print(f'matched: {typeerr_group.exceptions}')
if valerr_group:
print(f'matched: {valerr_group.exceptions}')
if rest:
print('re-raise rest exception group')
raise rest
ExceptionGroupクラスにはsplitメソッドがある。これはExceptionGroup例外がラップしている例外を、引数に指定した条件にマッチするものとマッチしないものに分けて返送する。上のコードはこのメソッドを使ってTypeError例外、ValueError例外、その他の例外に分けて、TypeError例外とValueError例外は処理をして、その他の例外は再送出するようにしている。
実行結果を以下に示す(try節は省略)。
... except ExceptionGroup as eg:
... print(eg.exceptions)
... typeerr_group, rest = eg.split(lambda e: isinstance(e, TypeError))
... valerr_group, rest = rest.split(lambda e: isinstance(e, ValueError))
... if typeerr_group:
... print(f'matched: {typeerr_group.exceptions}')
... if valerr_group:
... print(f'matched: {valerr_group.exceptions}')
... if rest:
... print('re-raise rest exception group')
... raise rest
...
(TypeError('TypeError in ExceptionGroup'), ValueError('ValueError in ExceptionGroup'))
matched: (TypeError('TypeError in ExceptionGroup'),)
matched: (ValueError('ValueError in ExceptionGroup'),)
最初の出力はexceptions属性に格納されている例外の一覧だ(直前の例と同じ)。その後の2行はTypeError例外とValueError例外の処理が行われたことを示している。
この他にも、exceptions属性を走査して、if文やmatch文で例外の型をチェックして、個別に処理していくといった方法も考えられるが、ここではsplitメソッドを使ってみた。
次にexcept*節で処理をしなかった例外があったらどうなるかを見てみよう。
try:
raise ExceptionGroup(
'exception group',
[
TypeError('TypeError in ExceptionGroup'),
ValueError('ValueError in ExceptionGroup'),
RuntimeError('RuntimeError in ExceptionGroup')
]
)
except* TypeError as e:
print(e.exceptions)
except* ValueError as e:
print(e.exceptions)
この例ではこれまでのTypeError例外とValueError例外に加えてRuntimeError例外も送出するようになっている。これを実行すると、次のような結果になる。
>>> try:
... raise ExceptionGroup(
... 'exception group',
... [
... TypeError('TypeError in ExceptionGroup'),
... ValueError('ValueError in ExceptionGroup'),
... RuntimeError('RuntimeError in ExceptionGroup')
... ]
... )
... except* TypeError as e:
... print(e.exceptions)
... except* ValueError as e:
... print(e.exceptions)
...
(TypeError('TypeError in ExceptionGroup'),)
(ValueError('ValueError in ExceptionGroup'),)
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 2, in <module>
| ExceptionGroup: exception group (1 sub-exception)
+-+---------------- 1 ----------------
| RuntimeError: RuntimeError in ExceptionGroup
+------------------------------------
RuntimeError例外は処理していないので、これが上位に伝播され、これを処理する部分がなかったのでトレースバックが表示された。try〜except*節が想定していなかった例外が発生した場合にそれを処理するコードは一般にはtry文をネストさせて次のようになる。
try:
try:
raise ExceptionGroup(
'exception group',
[
TypeError('TypeError in ExceptionGroup'),
ValueError('ValueError in ExceptionGroup'),
RuntimeError('RuntimeError in ExceptionGroup')
]
)
except* TypeError as e:
print(e.exceptions)
except* ValueError as e:
print(e.exceptions)
except ExceptionGroup as eg:
print(eg.exceptions)
先ほど見たexcept*節で個々の例外を処理するコードの外側にtry〜except文を置き、そこでExceptionGroup例外を捕捉するようにしている(内側で未処理の例外があれば、それもExceptionGroup例外にラップされて上位に伝播することに注意)。
ここまでに見てきたのが、ExceptionGroup例外を処理する基本的な方法といえるだろう。次に、実際に並行的に幾つかの処理を行うコードから1つ以上の例外が発生した場合のコードについて考えてみよう。
Copyright© Digital Advantage Corp. All Rights Reserved.