ジェネレータイテレータのthrow/closeメソッド、ジェネレータを簡潔に記述できるジェネレータ式、他のイテレータに処理を委譲するyield from式を取り上げる。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
前回はジェネレータ関数とジェネレータイテレータの基本的な話を取り上げた。今回は、ジェネレータイテレータが持つ残りのメソッドや、よりシンプルにジェネレータイテレータを生成可能な「ジェネレータ式」、別のイテレータに処理を委譲するyield from式について見ていこう。
前回はジェネレータ関数によって作成されるジェネレータイテレータが持つ__next__メソッドとsendメソッドについて見た。前者はジェネレータイテレータから値を取り出して次の要素に進むために、後者はそれに加えてジェネレータイテレータに値を送信するために使用する。
ジェネレータイテレータ(generatorクラス)には、それ以外に2つのメソッドがある。
throwメソッドは何らかの理由からジェネレータイテレータに対して例外を発生させる必要が生じたときに使う。このとき、発生させた例外をジェネレータイテレータ内部で処理して、実行を続けると、yield式によって次の値が呼び出した側に返送される。yield式に到達しなければ、StopIteration例外が発生する。
closeメソッドはジェネレータ関数のコード末尾に達するよりも前に、ジェネレータイテレータが削除される事態が発生した際に自動的に呼び出される。これは主にジェネレータイテレータ内部で使用しているリソースを解放するのに使える。リソースを使用するコードをtry文で囲み、finally節にリソースを解放するコードを記述しておくことで、ジェネレータイテレータが利用するリソースを安全に解放できる。
closeメソッドによりGeneratorExit例外が発生するが、その際に例外をどう処理するかは幾つか考えがある。シンプルなのは、GeneratorExit例外を捕捉せずにfinally節に後始末のコードを書いておくことだ。そうすると、finally節の実行が終わった時点でその例外が再度発生し、closeメソッドへと伝えられる。ただし、closeメソッドはこれをうまく処理して、呼び出し側に例外を伝えることなく、次のコードへと実行に移るようになっている。
あるいは、GeneratorExitを捕捉するexcept節を記述して、そこで後始末を行い、その後、GeneratorExit例外をもう一度発生させてもよい(finally節で後始末を行うと、throwメソッドによる例外処理でもそのコードが実行されるが、それだと都合が悪い場合があるかもしれない)。
以下に簡単な例を示す。
def sample_geniter():
counter = 0
while True:
try:
yield counter
counter += 1
except TypeError as e:
break
except Exception as e:
print(e)
counter += 1
finally:
print('finally')
このコードは無限にカウントアップするジェネレータイテレータを返すジェネレータ関数の定義だ。ジェネレータイテレータ内部では、TypeError例外が発生したらbreak文で無限ループを終了する。例外を処理してもyield式は実行されないので、そこでStopIteration例外が発生する(最初に思いついた例外を例に選んだだけで、TypeError例外であることに意味はない)。
それ以外の例外が発生すると、「except Exception as e」節でそれらが捕捉され、例外が処理されるが、このときには例外の内容を示して、カウンター変数(counter)の値を1つ増やすだけだ。結果、次のループが開始されて、yield式に到達するので、次の値が呼び出し側に戻される。
実はGeneratorExit例外は、上の2つのexcept節では捕捉されない(これはGeneratorExit例外がBaseExceptionクラスから派生するクラスで、Exceptionクラス以下の通常の例外クラス階層には含まれていないからだ)。よって、closeメソッドが呼び出されて、ジェネレータイテレータ内部でGeneratorExit例外が発生すると、それが処理されないまま、finally節が実行され、そこで同じ例外が再発生する。先に述べたように、closeメソッドがこれをうまく処理してくれるので、呼び出し側では例外が発生しないようになっている。
実際にこのジェネレータ関数を使ってみよう。まずはthrowメソッドで例外を発生させてみる。
mygeniter = sample_geniter()
print(next(mygeniter))
print(mygeniter.throw(NameError, 'name error'))
print(mygeniter.throw(TypeError, 'type error'))
このコードに示したように、throwメソッドは例外クラスの型を第1引数に、そのインスタンスに含める値を第2引数に指定して呼び出す(または、throw(NameError('name error'))のように呼び出してもよい)。
既に述べたようにTypeError例外を発生させると、最終的にStopIteration例外が発生するようなコードにしたので、まずはNameError例外を、その次に、TypeError例外をジェネレータイテレータ内部で発生させている。
実行すると、その結果は次のようになる。
上の画像に示した通り、NameError例外を発生させると、yield式によって次の値が呼び出し側に戻される(上の画像の「1」という表示)。TypeError例外を発生させると、yield式に到達しなかったためにStopIteration例外が発生したことが分かる。
次に、closeメソッドを(間接的に)呼び出してみよう。
mygeniter = sample_geniter()
next(mygeniter)
del mygeniter # 変数mygeniterを削除すると、自動的にcloseメソッドが呼び出される
print('hello')
このコードでは、ジェネレータイテレータを作成した後に、それを代入した変数をdel文で削除している。これにより、変数mygeniterとジェネレータイテレータオブジェクトの束縛が解除され、そのオブジェクトはもう使えなくなる。その時点で、closeメソッドが自動的に呼び出されて、ジェネレータイテレータ内部でGeneratorExit例外が発生する。上のsample_geniterジェネレータ関数定義の本文では、これを処理していなかったので、通常の例外処理の流れに従ってfinally節だけで実行されて、そこで最後にGeneratorExit例外が再発生する。closeメソッドがうまくこれを処理してくれるので、例外は呼び出し側には伝えられることなく、実行が次の行に移り、「hello」と表示される。
実行結果を以下に示す。
ここで次のようなコードについても考えてみよう。
mygeniter = sample_geniter()
next(mygeniter) # 上で作成したジェネレータイテレータのコードの実行開始
mygeniter = sample_geniter() # 同じ変数に別のオブジェクトを代入
実行結果は次のようになる。
「finally」と表示されたことに驚いた人もいるかもしれない。なぜこうなるかを少し考えてみよう。
このコードでは、1行目でジェネレータ関数を呼び出して、ジェネレータイテレータを手に入れて、2行目でそのコードの実行を開始している。そして、コードの末尾まで実行を行わないまま、3行目で同じ変数に再度ジェネレータイテレータを新規に作成して代入している。そのため、1行目で作成したジェネレータイテレータはもう使えない状態になる。そこで、closeメソッドが呼び出されるので、「finally」と突然表示される。これはPythonにおけるオブジェクトの寿命と関係するが、本連載では取り上げない。より高度な話題を取り上げる際に、オブジェクトの寿命については取り上げるつもりだ。
次にジェネレータ式を簡単に紹介する。ジェネレータ式とは、ジェネレータ関数を簡潔に記述するための記法で、かっこ「()」に囲んで記述する。基本的な構文は「(式 for 変数 in 反復可能オブジェクト)」である(これは内包表記によく似ている。ただし、かっこ「()」で囲んでいるが、これはタプルの内包表記ではなく、ジェネレータ式であることに注意しよう)。
例えば、0、1、2、3という整数値を順番に取り出すジェネレータ式は次のように書ける。
gen_expr = (x for x in range(4))
for num in gen_expr:
print(num)
実行結果を以下に示す。
同じことをジェネレータ関数を使って記述すると次のようになる(実行結果は省略)。
def sample_geniter():
yield from range(4)
gen_expr = sample_geniter()
for num in gen_expr:
print(num)
ジェネレータ式の方が簡潔に同じことを記述できていることに注目しよう。
if節によるフィルタリングも可能だ。例えば、指定した範囲にある整数値の中で、偶数だけを取り出すジェネレータ式を返すラムダ式は次のようになる。
sample_geniter = lambda x, y: (z for z in range(x, y) if z % 2 == 0)
for num in sample_geniter(0, 5):
print(num)
実行結果は次のようになる。
次に、yield from式について見ていこう。
Copyright© Digital Advantage Corp. All Rights Reserved.