curious course on coroutines and concurrencyのメモ。

これは、asyncioがなかった時代に、yield構文を使って、非同期プログラミングを解説した講座。Pyconの講演だったらしい。

yieldとcoroutine

普通、yieldはiteratorを定義するために使われる。こんな感じ:

def rep(n):
  x = 0
  while x < n:
    yield x
    x += 1

for i in rep(3):
  print(i)

0
1
2

このジェネレーターには、実は、「外から値を渡す」ことができる。こんな感じ:

def receive():
  while True:
    x = yield
    print(x)

r = receive()
next(r)
for i in range(3):
  r.send(i)
0
1
2

値の送受信を両方とも書くこともできる。こんな感じ:

def rands():
  r = yield "S"
  print(r)

rs = rands()
s = rs.send(None)
print(s)
rs.send("R")

S
R
Traceback (most recent call last):
  File "/Users/hotoku/junk/2022/03/24-080501.py", line 9, in <module>
    rs.send("R")
StopIteration

このプログラムは、次のように動作する。

  1. rs = rands()でジェネレーターrsが作られる(ここでは、randsの中のコードは実行されない)
  2. s = rs.send(None)で、次のようなことが起こる
    1. randsがスタートし、yield "S"まで進む
    2. このyieldされた値"S"が、nextの返り値となる
    3. グローバルの変数sに上の返り値が代入される
  3. print(s)"S"が表示される
  4. rs.send("R")で、次のようなことが起こる
    1. sendの引数"R"が、yield "S"の返り値となり、randsのロケール変数rに代入される
    2. print(r)"R"が表示される
    3. ジェネレーターの最後に到達したので、StopIterationが送出される
  5. 誰もStopIterationを受け取っていないので、プログラムが例外終了する

つまり、

  1. <generator>.send(None)で、グローバル→ジェネレーターに制御が移り、ジェネレーターの処理が開始される
  2. yield <value>で、ジェネレーター→グローバルに制御が戻る
  3. <generator>.send(v)で、グローバル→ジェネレーターに制御が移る

ということが繰り返されることが分かる。

また、ジェネレーターから、グローバルに処理の終了を知らせるにはStopIterationが使われていることが分かる。 逆に、グローバルからジェネレーターに終了を知らせるには、<generator>.close()を呼び出す。すると、待機しているyieldの行でGeneratorExit例外が発生する。こんな感じ:

def loop():
    try:
        while True:
            v = yield
            print(v)
    except GeneratorExit:
        print("finished")

l = loop()
l.send(None)
l.send(1)
l.send(2)
l.send(3)
l.close()
1
2
3
finished