[Python入門]ジェネレータの高度な話題Python入門(1/2 ページ)

ジェネレータイテレータのthrow/closeメソッド、ジェネレータを簡潔に記述できるジェネレータ式、他のイテレータに処理を委譲するyield from式を取り上げる。

» 2019年11月22日 05時00分 公開
[かわさきしんじDeep Insider編集部]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「Python入門」のインデックス

連載目次

 前回はジェネレータ関数とジェネレータイテレータの基本的な話を取り上げた。今回は、ジェネレータイテレータが持つ残りのメソッドや、よりシンプルにジェネレータイテレータを生成可能な「ジェネレータ式」、別のイテレータに処理を委譲するyield from式について見ていこう。

ジェネレータイテレータが持つメソッド

 前回はジェネレータ関数によって作成されるジェネレータイテレータが持つ__next__メソッドとsendメソッドについて見た。前者はジェネレータイテレータから値を取り出して次の要素に進むために、後者はそれに加えてジェネレータイテレータに値を送信するために使用する。

 ジェネレータイテレータ(generatorクラス)には、それ以外に2つのメソッドがある。

  • throwメソッド:yield式によって実行が中断した箇所で、引数に指定した例外を発生させる
  • closeメソッド:yield式によって実行が中断した箇所で、GeneratorExit例外を発生させる

 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')

throwメソッドとcloseメソッドに対応しているジェネレータ関数

 このコードは無限にカウントアップするジェネレータイテレータを返すジェネレータ関数の定義だ。ジェネレータイテレータ内部では、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メソッドの利用例

 このコードに示したように、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')

ジェネレータイテレータを代入した変数を削除して、closeメソッドを間接的に呼び出す

 このコードでは、ジェネレータイテレータを作成した後に、それを代入した変数をdel文で削除している。これにより、変数mygeniterとジェネレータイテレータオブジェクトの束縛が解除され、そのオブジェクトはもう使えなくなる。その時点で、closeメソッドが自動的に呼び出されて、ジェネレータイテレータ内部でGeneratorExit例外が発生する。上のsample_geniterジェネレータ関数定義の本文では、これを処理していなかったので、通常の例外処理の流れに従ってfinally節だけで実行されて、そこで最後にGeneratorExit例外が再発生する。closeメソッドがうまくこれを処理してくれるので、例外は呼び出し側には伝えられることなく、実行が次の行に移り、「hello」と表示される。

 実行結果を以下に示す。

実行結果 実行結果

 ここで次のようなコードについても考えてみよう。

mygeniter = sample_geniter()
next(mygeniter)  # 上で作成したジェネレータイテレータのコードの実行開始
mygeniter = sample_geniter()  # 同じ変数に別のオブジェクトを代入

closeメソッドは、オブジェクトと変数の束縛が解除されると呼び出される

 実行結果は次のようになる。

実行結果 実行結果

 「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)

range(0, 4)の範囲に含まれる整数値を取り出すジェネレータ式

 実行結果を以下に示す。

実行結果 実行結果

 同じことをジェネレータ関数を使って記述すると次のようになる(実行結果は省略)。

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式について見ていこう。

       1|2 次のページへ

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。