イテレータを戻り値とする「ジェネレータ関数」、それが返す「ジェネレータイテレータ」の使い方の基本について説明する。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
前回はPythonのイテレータについて見た後に、自分でイテレータを定義した。今回はイテレータを戻り値とするジェネレータ関数と呼ばれる機構について見ていこう。
ジェネレータ(ジェネレータ関数)とは、イテレータを作成するための関数のことだ。ジェネレータによって作成されたイテレータのことを特に「ジェネレータイテレータ」と呼ぶこともある。またはジェネレータ関数で作成されたイテレータのことを単純に「ジェネレータ」と呼ぶこともある。本稿では、イテレータを戻り値とする関数のことを「ジェネレータ関数」と、ジェネレータ関数によって作成されたことを「ジェネレータイテレータ」として表記する。
前回作成したイテレータは、__iter__メソッドと__next__メソッドを持つクラスとして定義した。これに対して、ジェネレータ関数は(その名の通り)関数の形で定義する。ただし、通常の関数とジェネレータ関数には大きな違いがある。
まず、通常の関数は、それを呼び出せば、その本体に記述したコードが実行されて、その結果が戻されるが、ジェネレータ関数を呼び出すと、戻ってくるのは、ジェネレータイテレータと呼ばれるオブジェクトであることだ(Pythonの処理系がジェネレータ関数の定義を処理する際に、そのように特別扱いしてくれる)。
作成されたジェネレータイテレータはイテレータなので、__iter__メソッドや__next__メソッドを持ち、前回見たイテレータを置ける場所(つまり、反復可能オブジェクトを置ける場所の大半)にそれを置けるようになっている。このとき、ジェネレータ関数定義の本体はおおよそ__next__メソッドの内容と考えられる。また、ジェネレータ関数により生成されたジェネレータイテレータが、本体に記述したコードの実行を制御するようになっている。
もう一つ、関数はそれを呼び出すごとに、本体に記述されたコードが一度実行され、return文で戻り値を返すか、あるいは関数末尾まで到達すると、そこで関数の実行が終了する。これに対して、ジェネレータ関数がその呼び出し側に値を返すには、return文ではなく「yield式」を使うところも大きな違いだ。しかも、ジェネレータ関数の本体に書いたコードは、呼び出し側に値を何度も返せる。この辺は文字で説明するよりもコードで見ていただいた方が理解が早いので、まずは簡単なジェネレータ関数を定義して、その動作を見てみよう。
簡単なジェネレータ関数の定義を以下に示す。
def simple_generator():
yield 1
yield 2
yield 3
既に述べた通り、return文ではなく、「yield 値」という「yield式」が書かれていることが分かる。これが文ではなく、式であることには理由があるが、それについては後で見よう。ここでは、yield式で呼び出し側に値を返せることを覚えておけばよい。そして、上で「呼び出し側に値を何度も返せる」と書いたことに対応するのが、yield式が3つ並んでいる点だ。
では、上のコードで定義したsimple_generator関数(と、それが返すジェネレータイテレータ)の使い方を見ていく。
まずは、ジェネレータ関数を呼び出して、ジェネレータイテレータを取得するところから始めよう。
mygeniter = simple_generator()
print(type(mygeniter)) # simple_generator関数の戻り値の型を調べる
print('__iter__' in dir(mygeniter)) # __iter__メソッドがあるか
print('__next__' in dir(mygeniter)) # __next__メソッドがあるか
最初の行では、上で定義したジェネレータ関数を呼び出している。その戻り値であるジェネレータイテレータが変数mygeniterに代入される。2行目では、その型を調べている。3行目と4行目では、ジェネレータイテレータに本当に__iter__メソッドと__next__メソッドがあるかどうかを確認している。
このコードを実行すると次のようになる。
ご覧の通り、simple_generator関数の戻り値の型は「generator」になっている(このことから、ジェネレータイテレータのことを単に「ジェネレータ」と呼ぶことがあるのだろう)。また、その下の出力はいずれもTrueであることから、これがイテレータであることも確認できた。この__next__メソッドから、ジェネレータイテレータのコード(ジェネレータ関数定義の本文として記述したコード)が実行される。
ジェネレータ関数の戻り値がイテレータであることが分かったので、next関数にこれを渡したり、__next__メソッドを呼び出したりしてみよう。
print(next(mygeniter)) # 次の値を取得
print(mygeniter.__next__()) # 次の値を取得
print(next(mygeniter)) # 次の値を取得
print(mygeniter.__next__()) # 次の値を取得
これを実行すると、次のようになる。
「yield 1」「yield 2」「yield 3」という3つのyield式に渡している整数値が順番に返された後にStopIteration例外が発生している。
通常の関数では、その定義の本体に書いたコードは関数末尾に到達するか、どこかでreturn文に到達するまで実行が続けられる。これに対して、ジェネレータ関数の本体に書いたコードは、そのジェネレータイテレータの__next__メソッドが呼び出されることで実行される。そして、その実行はyield式に到達するまで続けられ、yield式が実行されると、それに渡された値が呼び出し側に戻され、そこでコードの実行が一時停止されるのだ。その後、再び、ジェネレータイテレータの__next__メソッドが呼び出されると、今度は次の行から実行が再開されて、再びyield式に到達するまで実行が続けられる。このような形で、ジェネレータイテレータのコードは実行されることをまずは覚えておこう。
上のコードなら、以下のような形でジェネレータ関数に記述したコードは実行されているということだ。
このようにしてコードの実行と中断が繰り返され、最終的にyield式に到達しないままコードの実行が終了したところで、StopIteration例外が発生する。コードが実行/中断されている間の情報(ローカル変数の値や、現在どこのコードを実行しているかなど)はジェネレータイテレータ自身が管理してくれるので、プログラマーが気にする必要はない。
次に、先ほどのsimple_generator関数のコードを次のように変更してみよう。
def simple_generator():
yield 1
print('first yield expression done')
yield 2
print('second yield expression done')
yield 3
print('third yield expression done')
そして、今度はnext関数(__next__メソッド)を一度だけ呼び出してみよう。
mygeniter = simple_generator()
print(next(mygeniter)) # 次の値を取得
すると、次のような結果が得られる。
「first yield expression done」が表示されると思った方もいるかもしれない。しかし、yield式が実行されたところで、実行の制御も呼び出し側に戻るので、直下のprint関数呼び出しは実行されないのだ。そして、もう一度、next関数を呼び出したところで、次の行から実行が再開される。
print(next(mygeniter)) # 次の値を取得
今度は最初のyield式の次の行が実行されて、次のyield式まで実行が続けられるので、次のような結果になる。
次に前回に見た0から順にカウントアップしていくだけのイテレータを、ジェネレータ関数を使ってジェネレータイテレータとして書き直してみよう。
Copyright© Digital Advantage Corp. All Rights Reserved.