python の awync/await の動きについて、 yield from から辿って見ていく
python 3.5 から導入された async/await 構文について、 どういったものなのかちゃんと知っておきたかったので、調べてみた。
関連する PEP を読んだり Future
の実装を見たりとかしたところの理解としては、
動作としては generator,
yield from
と同じようなもの構文やら要求・提供するインターフェースは別物
(Iterator と見分けやすくした。)
ついでに、
with
やfor
のための便利構文・機能を足した
という感じっぽい。
この「 yield from
と同じようなもん」という理解であっているのかを今回は確認していく。
まずは yield from について
元となったのは yield from
(generator) であろうという理解だ。まずはコイツの動作を確認する。
yield from
は後ろにIterable
(sub generator) をとり、そこからIterator
を取り出す (.__iter__()
を呼び出す)Iterator
を進めて、出てきた要素を外側 (generator を進めている側) に戻す外側が generator を進める (
.__next__()
や.send()
を呼び出す) と、Iterator
の止まっていた部分から動作を再開する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 と非同期処理をうまく組み合わせると便利 (いちいちコールバック関数書かなくて良い、同期的処理っぽく書ける) というのは間違いないのだけど、微妙に紛らわしい名前ではある。