クラスを使って定義したカウントアップするだけのイテレータは次のようなものだった。
class CountUpIterator:
def __init__(self, limit=5):
self.limit = limit
self.counter = -1
def __iter__(self):
print('__iter__ method called')
return self
def __next__(self):
print('__next__ method called')
self.counter += 1
if self.counter >= self.limit:
raise StopIteration()
return self.counter
ここでは__init__メソッドでカウンターの上限値を指定して、__next__メソッドで現在値と上限値を比較して、上限に達していなければ、現在値を返して、その値を1増やすようにしていた。上限値の指定は、ジェネレータ関数のパラメーターに受け取ることにすれば、ジェネレータ関数は次のように書ける。クラスを使ったときよりもシンプルに、その動作だけを書けていることに注目しよう。
def countup_geniter(limit=5):
counter = -1
while True:
counter += 1
if counter >= limit:
break
yield counter
CountUpIteratorクラスでは、__next__メソッドは呼び出されるたびに、そのコードが実行されるので、現在値(self.counter)と上限値(self.limit)をインスタンス変数として管理していた。一方、ジェネレータ関数では、それらをローカル変数やパラメーターを使って現在値(counter)と上限値(limit)を管理して、それを無限ループ(「while True:」のブロック)の中で操作するようにしている。
無限ループの中でyield式が実行されると、現在値(と実行の流れ)が呼び出し側に戻され、次のnext関数(__next__メソッド)呼び出しにより、yield式の次の行、つまり次の無限ループの先頭に移り、次のループが始まる。これにより、上限に到達するまでは、ジェネレータイテレータが連続的に値を生成(ジェネレート)するようになっている。
現在値と上限値を比較したときに現在値が上限値に達していれば、クラスベースのCountUpIteratorでは「raise StopIteration()」として自ら例外を発生させていたが、上のコードではbreak文で無限ループを終了している(あるいは、「return」とだけ書いて、関数本体の実行を終了してもよい)。
これは、先ほども述べたように「yield式に到達しないままコードの実行が終了したところで、StopIteration例外が発生する」からだ。自分でStopIteration例外を発生させる必要はない点には注意しよう。上のコードをbreak文から「raise StopIteration()」に変更すると、環境によっては次のようにRuntimeError例外が発生する(Python 3.7.3以降でのデフォルトの振る舞い。それ以前のバージョンでも警告メッセージが表示されるかもしれない。詳細はPEP 479「Change StopIteration handling inside generators」を参照されたい)。
使用例を以下に示す。
for num in countup_geniter(3):
print(num)
ジェネレータイテレータはもちろんイテレータなので、このようにfor文でも利用可能だ。実行結果を以下に示す。
なお、上のコードではクラスを使ったイテレータと似たロジックをジェネレータ関数の本体に書いているが、これは次のようにも書ける。
def countup_geniter(limit=5):
counter = 0
while True:
if counter >= limit:
break
yield counter
counter += 1
ここでは現在値を表す変数counterの初期値を「0」として、その値を1つ増やす行を本体の最下部に移している。このように書けるのは、ジェネレータイテレータでは、yield式で制御が呼び出し側に戻った後も、その内部でコードのどこが実行されているかが管理されていて、次にnext関数(__next__メソッド)が呼び出されて、制御がジェネレータイテレータに戻ったときには、yield式の次の行(ジェネレータ関数定義の本体の最終行にある「counter += 1」)が実行された後に、次のループが始まるようになるからだ。クラスベースのCountUpIteratorの__next__メソッドでは「return self.counter」行の後に「self.counter += 1」と書いても、その行が実行されることはないので、これと全く同じ書き方はできないことに気を付けよう。
ところで、先ほど、「yield 値」は「yield式」だと述べた(これは「return 値」が「return文」であるのと対照的だ)。これは、「yield 値」が戻り値を持つからだ(なお、式は文でもあるので、単純に「yield 値」と書いた場合、それはyield文でもある)。
yield式の値は、ジェネレータイテレータがどのような形で実行を再開するかによる。今までに見てきたnext関数(ジェネレータイテレータの__next__メソッド)から実行が再開された場合、その値はNoneとなる。式なので、その値は代入演算子を使って任意の変数に代入できる。
実際に見てみよう。以下は最初に書いたsimple_generatorジェネレータ関数を書き直したものだ。
def simple_generator():
value = yield 1
print('value of first yield:', value)
value = yield 2
print('value of second yield:', value)
value = yield 3
print('value of third yield:', value)
ここでは「value = yield 値」としてyield式の値を変数valueに代入している。式の値は、ジェネレータイテレータの実行が再開された時点で変数valueへと代入される(よって、最初に変数valueに値が代入されるので、二度目のnext関数呼び出しが行われた時点だ)。yield式の値が変数に代入されるのは、ジェネレータイテレータの実行が再開された後であることに注意しよう。
では、実際の動作を確認してみよう。
mygeniter = simple_generator()
print(next(mygeniter))
このコードを実行すると「yield 1」が実行され、実行の制御が呼び出し側に戻される。この時点では、変数valueには値は代入されていないし、yield式の次のコードも実行されていない。よって、実行結果は次のようになる。
そして、もう一度、以下のコードを実行すると次のように、変数valueの値(None)が表示される。
print(next(mygeniter))
これは、実行がジェネレータイテレータに戻り、「yield 1」という式の値(None)が変数valueに代入され、それがprint関数で呼び出され、最後に「yield 2」で値「2」が呼び出し側に返されたということだ。
このようにnext関数(あるいはfor文)でジェネレータイテレータを操作している間は、yield式の値は常にNoneとなる。だが、yield式の値は呼び出し側から設定できる。これにはジェネレータイテレータが持つsendメソッドを使用する。sendメソッドは__next__メソッドと同様に、ジェネレータイテレータの実行を再開するのに使える。sendメソッドに何かの値を渡すと、実行がジェネレータイテレータの側に移り、渡した値が直前に実行したyield式の値となるのだ。実際に試してみよう。
print(mygeniter.send('hello'))
これを実行すると次のように、送信した文字列'hello'が表示される。
これだけ見ても、あまりメリットは感じられないが、呼び出し側がジェネレータイテレータの処理に関与できるということだ。例えば、次のようなジェネレータ関数を定義してみよう。
def hellomsg_geniter():
namelist = ['kawasaki', 'isshiki', 'endo']
counter = 0
length = len(namelist)
value = None
while True:
if value: # sendメソッドで名前が送信されてきたときの処理
namelist.append(str(value))
length += 1
counter = length - 1
value = yield 'hello ' + namelist[counter]
counter += 1
if counter % length == 0: # リストの末尾要素を使ったら先頭から使う
counter = 0
これは「hello ○○」というメッセージを永遠に生成し続けるジェネレータイテレータを返すジェネレータ関数の定義だ。next関数(__next__メソッド)やsendメソッドで、実行を開始(再開)するたびに内部に持つ名前リストの要素を含んだハローメッセージをyieldする(カウンターで現在値を管理して、リスト末尾の要素をyieldしたら、先頭の要素に戻るようにしている)。
このとき、sendメソッドで名前をジェネレータイテレータに送信すると、それが内部で使用している名前リストに追加されるとともに、それがyieldされ、その次には名前リストの先頭の要素が使われる。
以下に、使用例を示す。
mygeniter = hellomsg_geniter()
print(next(mygeniter))
print(next(mygeniter))
print(next(mygeniter))
print(next(mygeniter))
print(mygeniter.send('deep insider'))
print(next(mygeniter))
このコードは、ジェネレータ関数を呼び出してジェネレータイテレータを作成した後に、next関数でその値をまず4回取り出している。これで名前リストの要素が循環的に使用されているのが確認できるはずだ。次に、sendメソッドでジェネレータイテレータに文字列'deep insider'を送信している。これにより、その名前がリストに追加されて、yieldされた後に(そうするためにカウンターの値も操作している)、名前リストの先頭からまた名前が使用されているのが分かるはずだ。
このように、呼び出す側からジェネレータイテレータの処理に関与する場合にはsendメソッドを使用する。なお、ジェネレータイテレータのコードの実行を開始するには、通常、next関数(あるいはfor文内部での同様な機構)が使われるが、最初からsendメソッドを使うことも可能だ。ただし、このときには引数にNoneを指定する必要がある(そうしないと、TypeError例外が発生する)。
ここまでに見てきた__next__メソッドとsendメソッドに加えて、ジェネレータイテレータ(generatorクラス)にはこの他にも幾つかのメソッドがあるが、これらについては次回に取り上げる。
今回はPythonのジェネレータ関数とそれが生成するジェネレータイテレータの基礎を取り上げた。次回はジェネレータイテレータが持つ他のメソッドや、yield from式、ジェネレータ式と呼ばれる記法について見ていこう。
Copyright© Digital Advantage Corp. All Rights Reserved.