memo

2016-06-25

python の awync/await の動きについて、 yield from から辿って見ていく

python 3.5 から導入された async/await 構文について、 どういったものなのかちゃんと知っておきたかったので、調べてみた。

関連する PEP を読んだり Future の実装を見たりとかしたところの理解としては、

  • 動作としては generator, yield from と同じようなもの

  • 構文やら要求・提供するインターフェースは別物

    (Iterator と見分けやすくした。)

  • ついでに、 withfor のための便利構文・機能を足した

という感じっぽい。

この「 yield from と同じようなもん」という理解であっているのかを今回は確認していく。

まずは yield from について

元となったのは yield from (generator) であろうという理解だ。まずはコイツの動作を確認する。

  1. yield from は後ろに Iterable (sub generator) をとり、そこから Iterator を取り出す (.__iter__() を呼び出す)

  2. Iterator を進めて、出てきた要素を外側 (generator を進めている側) に戻す

  3. 外側が generator を進める (.__next__().send() を呼び出す) と、 Iterator の止まっていた部分から動作を再開する

  4. Iterator が sub generator の場合、そこから return された値が yield from の結果として generator に渡る

実際のコードでも見てみる。

def sub_generator():
    print('    sub generator 開始')
    print('    ... そして値を yield します。')
    ret = yield 1
    print('    sub generator が再開し、 send された値を受け取りました:', ret)
    print('    ... そして別の値を return します。')
    return 42


def generator():
    print('  generator 開始')
    print('  ... そして sub generator を yield from します。')
    ret = yield from sub_generator()
    print('  generator が再開し、 sub generator から値を受け取りました:', ret)
    print('  何か値を return し、終了します。')
    return 'done'


print('generator を作って、')
g = generator()
print('... そして開始します。')
ret = g.send(None)
print('(sub) generator が停止し、そこから yield された値を受け取りました:', ret)
print('generator を再開します。')

try:
    g.send(2)

except StopIteration as e:
    # generator は終了時に StopIteration 例外を起こし、
    # return された値はその `value` attribute に入ります。
    print('generator が終了し、値を返しました:', e.value)

else:
    assert False

実行結果:

generator を作って、
... そして開始します。
  generator 開始
  ... そして sub generator を yield from します。
    sub generator 開始
    ... そして値を yield します。
(sub) generator が停止し、そこから yield された値を受け取りました: 1
generator を再開します。
    sub generator が再開し、 send された値を受け取りました: 2
    ... そして別の値を return します。
  generator が再開し、 sub generator から値を受け取りました: 42
  何か値を return し、終了します。
generator が終了し、値を返しました: done

yield from その2

さて、上記では sub generator を作るのに普通に generator を使ったが、 ここはユーザー定義の Iterable でも構わない。

以下では自分で定義した Iterable で sub generator を置き換えてみている。 この場合でも上記と同様の動作をするはず。

class MyIterable(object):
    # Iterable は __iter__ を実装し、そこから Iterator を返す。
    # Iterator の作成には generator を利用する。
    def __iter__(self):
        print('    my iterable 開始')
        print('    ... そして値を yield します。')
        ret = yield 1
        print('    my iterable が再開し、 send された値を受け取りました:', ret)
        print('    ... そして別の値を return します。')
        return 42


def generator():
    print('  generator 開始')
    print('  ... そして my iterable を yield from します。')
    ret = yield from MyIterable()
    print('  generator が再開し、 my iterable から値を受け取りました:', ret)
    print('  何か値を return し、終了します。')
    return 'done'


print('generator を作って、')
g = generator()
print('... そして開始します。')
ret = g.send(None)
print('generator (my iterable) が停止し、そこから yield された値を受け取りました:', ret)
print('generator を再開します。')

try:
    g.send(2)

except StopIteration as e:
    # generator は終了時に StopIteration 例外を起こし、
    # return された値はその `value` attribute に入ります。
    print('generator が終了し、値を返しました:', e.value)

else:
    assert False

実行結果:

generator を作って、
... そして開始します。
  generator 開始
  ... そして my iterable を yield from します。
    my iterable 開始
    ... そして値を yield します。
generator (my iterable) が停止し、そこから yield された値を受け取りました: 1
generator を再開します。
    my iterable が再開し、 send された値を受け取りました: 2
    ... そして別の値を return します。
  generator が再開し、 my iterable から値を受け取りました: 42
  何か値を return し、終了します。
generator が終了し、値を返しました: done

最初と同様の実行結果になることを確認できた。

そして async/await へ

では続いて async/await を見る。

最初に書いたように、 async/await は yield from と動作は同じだが、

  • def の代わりに async def, yield from の代わりに await と書く

  • Iterable の代わりに Awaitable をとる

  • Awaitable.__iter__() の代わりに .__await__() が呼ばれる

    (.__await__() が返すものは、 .__iter__() と同様 Iterator でよい。)

  • (呼び出し結果の coroutine は __iter__, __next__ を持たない (Iterable=/=Iterator ではない))

という違いがある。

では、上記 yield from の例その2を、これにしたがって書き換えてみよう。

class MyAwaitable(object):
    # __iter__ -> __await__
    def __await__(self):
        print('    my awaitable 開始')
        print('    ... そして値を yield します。')
        ret = yield 1
        print('    my awaitable が再開し、 send された値を受け取りました:', ret)
        print('    ... そして別の値を return します。')
        return 42


# def -> async def
async def coroutine():
    print('  coroutine 開始')
    print('  ... そして my awaitable を await します。')
    # yield from -> await
    ret = await MyAwaitable()
    print('  coroutine が再開し、 my awaitable から値を受け取りました:', ret)
    print('  何か値を return し、終了します。')
    return 'done'


print('coroutine を作って、')
coro = coroutine()
print('... そして開始します。')
ret = coro.send(None)
print('coroutine (my awaitable) が停止し、そこから yield された値を受け取りました:', ret)
print('coroutine を再開します。')

try:
    coro.send(2)

except StopIteration as e:
    # coroutine は終了時に StopIteration 例外を起こし、
    # return された値はその `value` attribute に入ります。
    print('coroutine が終了し、値を返しました:', e.value)

else:
    assert False

実行結果:

coroutine を作って、
... そして開始します。
  coroutine 開始
  ... そして my awaitable を await します。
    my awaitable 開始
    ... そして値を yield します。
coroutine (my awaitable) が停止し、そこから yield された値を受け取りました: 1
coroutine を再開します。
    my awaitable が再開し、 send された値を受け取りました: 2
    ... そして別の値を return します。
  coroutine が再開し、 my awaitable から値を受け取りました: 42
  何か値を return し、終了します。
coroutine が終了し、値を返しました: done

yield from と同様の動作をしていることが確認できた。

まとめ

以上、 generator から async/await へと変形していくことにより、両者が動作の上で違いがないことを確認できた。

(余談)

さて、動作として generator と同じであるということは、つまりこれは coroutine を作るための機能であって、 本質的には非同期処理とは関係ないということだ。

まあ、 coroutine と非同期処理をうまく組み合わせると便利 (いちいちコールバック関数書かなくて良い、同期的処理っぽく書ける) というのは間違いないのだけど、微妙に紛らわしい名前ではある。