ジェネレイタ, コルーチン (Python3対応)

(2017.7.30)

_ラムダ式, クロージャ

Pythonのλ式はクロージャ (閉包; closure) を生成する。

ただ、lambda 仮引数: の後ろに改行を入れられないので、式一つの関数しか作れない。制限が強すぎる。

クロージャなので、レキシカルスコープ (lexical scope) になっている。

Python
  1. x = 10
  2. sim = lambda y: y * x
  3. def fun(lam):
  4. x = 20
  5. return [ lam(r) for r in [1, 2, 3] ]
  6. print( fun(sim) )
実行結果:
[10, 20, 30]

クロージャを生成 (定義) した際の変数xがクロージャに閉じ込められ、呼び出される場所の変数ではなく、定義した場所の変数が使われる。変数セットなどの「環境」がクロージャに保存されている。

クロージャの型は class 'function' で、def文による関数と同じ。持っているメソッドも完全に同じ;

>>> dir(simple_fun)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

_ジェネレイタ

通常の関数が値を返すには return による。あと例外による脱出もあるが。

関数ブロックのなかに, 字面として yield があるときは、その関数の呼び出しでは、文が実行されずに, ジェネレイタ generator が生成される。

Python
  1. def simple_gen():
  2. yield "hoge"
  3. yield "fuga"
  4. gen = simple_gen() # 関数が実行されるのではなく、ジェネレイタを生成
  5. for s in gen:
  6. print( s )
実行結果:
hoge
fuga

ジェネレイタオブジェクトに対して __next__() メソッドを呼ぶと, 関数ブロックが実行され, yield式の場所でいったん実行が一時停止され、関数から戻る。__next__()でその直後から再開される。

関数呼び出しになるかジェネレイタ生成式になるか

注意したいのは, 関数が実行されるかジェネレイタになるかの条件、それから関数ブロックが一体いつ実行されるのか。

次の例では, yield式は決して実行されない。にも関わらず simple_gen()でジェネレイタが生成される。

Python
  1. def simple_gen():
  2. print("in simple_gen()!");
  3. if False:
  4. yield "hoge"
  5. yield "fuga"
  6. gen = simple_gen()
  7. print( type(gen) ) #=> <class 'generator'>
  8. # 実行
  9. print( next(gen) )
  10. print( gen.__next__() )

つまり、実行時ではなく構文解析の段階で、文が実行される関数になるか、ジェネレイタを生成する関数になるか、が決まる。そうすると、最初の関数呼び出しが無意味のように感じるが、ジェネレイタに実引数を渡せるようにするため、こうなっているのだろう。でも、首尾一貫性も何もあったもんじゃない。

ジェネレイタを生成する関数呼び出しでは、ブロック内の文はまったく実行されない。

中の文は、ジェネレイタに対して最初の __next__を呼び出したときに初めて実行され, 関数ブロックの始まりから最初の yield までが実行される。ジェネレイタからの戻り値は yield 式の引数。そこで、いったん実行が中断される。「環境」が保存される。

以降は、__next__() で yield式の直後から再開される。yield以外で関数ブロックから抜けると StopIteration 例外が発生する

ジェネレイタは次のメソッドを持つ。

['__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']

__iter____next__ を持っており、for文にイテレータとして渡すことができる。

コルーチン

途中で処理を中断し、再開できるような構造は「コルーチン (coroutine)」または「継続 (continuation)」と呼ばれる。Pythonのジェネレイタは、まさにコルーチン。継続はもっと強力に、どこからでも再開できるようなもの。See Coroutine vs Continuation vs Generator - Stack Overflow

ジェネレイタは __next__() で再開できるが、その時に外からジェネレイタの中に値を与えることもできる。__next__() の代わりに send()メソッドを使う。ジェネレイタの中で再開されたときに、yield式の値になる。

Python
  1. def gen_send():
  2. val = yield "hoge"
  3. yield val * 2
  4. gen = gen_send()
  5. print( gen.__next__() ) #=> hoge
  6. print( gen.send(10) )

実行結果:

hoge
20

ジェネレイタオブジェクトの __next__() で、関数の最初から最初のyieldまで実行される。この例では __next__() の戻り値は, "hoge".

send() メソッドで値を投げ込むことができる。この呼び出しでジェネレイタ内部で実行が再開される。外側のsend()の戻り値は、ジェネレイタ内の, 次のyield に与えられた引数。

一度も __next__() を呼び出していない状態で send() すると、TypeError が発生する。うーん。

_関数、イテレイタ、ジェネレイタで同様の処理

例として、フィボナッチ数を返す処理を作ってみる。

何も考えずに書くと、nが大きくなると急激に遅くなる。

Python
  1. import sys
  2. # Fibonacci数を返す. 強烈に遅い. n = 35 で 5.4秒ぐらい
  3. # @param n Fnを返す
  4. def fib(n):
  5. if n < 0:
  6. raise ValueError()
  7. if n < 2:
  8. return n
  9. return fib(n - 2) + fib(n - 1)
  10. argv = int(sys.argv[1])
  11. print( fib(argv) )

続いて、自作のイテレイタ. 値をメモしつつ全部生成する。Python って値を生成せずにイテレイタを進めることができない。

fib(n - 2), fib(n - 1)の無駄がないので、速度は上のよりは圧倒的。

Python
  1. import sys
  2. class FibIter:
  3. # n は個数. n = 2 なら, F0, F1 を返す.
  4. def __init__(self, n):
  5. if n < 0:
  6. raise ValueError()
  7. self.i = 0
  8. self.count = n
  9. self.prev2, self.prev = 0, 1
  10. def __iter__(self):
  11. return self
  12. def __next__(self):
  13. self.i += 1
  14. if self.i > self.count:
  15. raise StopIteration
  16. if self.i <= 2: # F0, F1
  17. return self.i - 1
  18. r = self.prev + self.prev2
  19. self.prev2, self.prev = self.prev, r
  20. return r
  21. argv = int(sys.argv[1])
  22. for i, n in enumerate(FibIter(argv + 1)):
  23. print(i, ':', n)

イテレイタオブジェクトは内部状態を持てるが、__next__() でつど脱出するため、何でもインスタンス変数で持たないといけない。selfまみれになる。

イテレイタと同じ処理をジェネレイタで書き直す。メソッドを跨がないので、コードがずいぶんコンパクトになる。

Python
  1. import sys
  2. # n は個数. n = 2なら F0, F1 を yield する.
  3. def fib_gen(n):
  4. if n < 0:
  5. raise ValueError()
  6. prev2, prev = 0, 1
  7. for i in range(n):
  8. if i < 2:
  9. yield i
  10. continue
  11. r = prev + prev2
  12. prev2, prev = prev, r
  13. yield r
  14. argv = int(sys.argv[1])
  15. for i, n in enumerate(fib_gen(argv + 1)):
  16. print(i, ':', n)