ジェネレータとは
ジェネレータとはイテレータを簡単に実装できる手段です(イテレータについては[Python] イテレータを実装する)。
イテレータはクラスを定義し、それに特別なメソッドを用意する必要があります。それに対してジェネレータは必要最小限のコードだけで実装できます。そして、使い方はイテレータと同じように使用できます。
ジェネレータの実装方法
ジェネレータは関数のような形をとりますが、値を返すのにreturnではなくyieldを使用します。それ以外に最も大きな違いは、yieldで値を返却した状態を保持するということです。状態を保持するとはどういうことでしょうか?下の例で見てましょう。
>>> def sample_generator():
... yield 1
... yield 2
... yield 3
...
ジェネレータを実装しました。yieldがたくさんあって変な感じがしますが、最初にそのまま呼び出してみます。
>>> sample_generator()
<generator object sample_generator at 0x0000000001E5D6D0>
そのまま呼び出すとオブジェクトが返却されましたが、そのままでは使用できないようです。返却されるオブジェクトを確認してみましょう。
>>> sg = sample_generator()
>>> type(sg)
<class 'generator'>
>>> dir(sg)
['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__','__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']
>>>
クラスはgeneratorのようです。そしてメソッドに__iter__、__next__があるのでイテレータと同じように組み込み関数next()で呼べそうです。それではnext()を使用して値を取り出します。
>>> next(sg)
1
>>> next(sg)
2
>>> next(sg)
3
>>> next(sg)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
一回目のnext()関数での呼び出しでは「yield 1」で1が返却されます。二回目のnext()関数での呼び出しでは、その下の「yield 2」で2が返却されます。つまり、普通の関数のように呼び出されるたびに関数の初めから実行されるのではなく、前回yieldで値を返却したその続きから処理を実行するということです(初回は初めから)。そして関数を抜ける際にはStopIterationが発生します。また、python3ではreturnがあればreturnに到達しても発生します。
このようにジェネレータは状態を保持しているので、実装する際にはそのことを意識する必要があります。ジェネレータはクラスを一つの関数で表現するようなものです。
まとめ
ジェネレータを実装するには
- 関数のような形にする
- 値の返却はreturnではなくyieldを使用する
- 呼び出されるたびに前回のyield以降から処理が行われることを意識する
ジェネレータでできること
まとめてしまいましたが、ジェネレータでは色々なことができます。いくつか見てみましょう。
途中で値を与える
ジェネレータでは実行途中にジェネレータに値を渡すことができます。**send()**関数を使って引数に値を設定すると、値をジェネレータに渡すのと同時にジェネレータから値を取得できます。次のサンプルを見てみましょう。
>>> def surround(str):
... s = str
... for i in range(5):
... v = yield f'{s}{i}{s}'
... if v is not None:
... s = v
...
0から4までの数字を、引数で与えた文字で囲って返却するジェネレータです。4行目のように書くと引数で与えた値がジェネレータに渡されます。
>>> sur = surround('*')
>>>
>>> next(sur)
'*0*'
>>> next(sur)
'*1*'
>>> sur.send('#')
'#2#'
>>> next(sur)
'#3#'
>>> next(sur)
'#4#'
>>>
最初はnext()で呼び出すと"*"で囲んだ数字が返却されます、途中でsend()を使用して"#"をジェネレータに与えると"#"で囲まれた数字が返却されることがわかります。
サブジェネレータへの委譲構文
ジェネレータの内部から別のジェネレータを呼ぶことができることです。実際に動かしてみて動作を確認してみましょう。まずはメインのジェネレータから呼ばれる側のジェネレータです。1,2,3と値を返すものと'a','b','c'と値を返すものになります。
def sub1():
yield 1
yield 2
yield 3
def sub2():
yield 'a'
yield 'b'
yield 'c'
そして呼び出す元のメインのジェネレータになります。呼び出す側ではyield fromで呼び出すジェネレータを指定します。
def generator():
yield 'A'
yield from sub1()
yield from sub2()
yield 'B'
for i in generator():
print(i)
これを実行すると以下のように出力されます。
A
1
2
3
a
b
c
B
前に説明した通り、ジェネレータは状態を保持しているので、__next__()を呼び出すと前回値を返却したyieldから処理を開します。
しかし、yield fromを指定している場合。メインジェネレータ内で処理がyield fromまで達すると、サブジェネレータに処理が移ります。そしてサブジェネレータ内のyieldに達すると、そこの値を返却します。そして、メインジェネレータの__next__()が呼ばれると前回値を返却したサブジェネレータのyieldから処理を継続します。サブジェネレータの処理を抜けたら、メインジェネレータに処理が戻ります。これがサブジェネレータへの委譲構文です。