Generator を使って Coroutine を実装
Python の Coroutine は便利そうな仕組みだが、いかんせん構文糖がきつすぎて何をやってるのか分かりにくいので、プリミティブな構文で再実装してみます。Python asyncio で取り上げた、一秒ずつ待ちながら数を数えるカウンターを Generator だけで作るとこうなります。
import asyncio
# 1 から max までの数を一秒ずつ数える。Generator 版
def counter_coroutine_generator(max):
count = 0
while count < max:
count += 1
print(count)
# 以下の行は yield from asyncio.sleep(1) のように書ける。
sleep_generator = asyncio.sleep(1)
sleep_future = next(sleep_generator)
yield sleep_future
return '数え終わりました'
counter = counter_coroutine_generator(3) # Generator 関数の返り値は Generator オブジェクト
print(f'counter is {counter}')
loop = asyncio.get_event_loop()
result = loop.run_until_complete(counter) # Generator 関数の return で返した物は loop.run_until_complete の返り値になる。
print(result)
loop.close()
実行結果
counter is <generator object counter_coroutine_generator at 0x1045de888>
1
2
3
数え終わりました
このように Coroutine を async def ではなく、Generator を使って書くと、Coroutine では
await asyncio.sleep(1)
のように書いた部分が Generator では
sleep_generator = asyncio.sleep(1)
sleep_future = next(sleep_generator)
yield sleep_future
のようになりました。ここはさらに短く yield from asyncio.sleep(1)
と書けます。さて、なぜ Coroutine の処理を待つ await
が Generator の次の値を受け取り(next) 処理を中断する (yield) となるかちょっと自分も分かっていません。多分ここで loop に処理を返し loop が一秒後に処理を再開されるのでは無いかと想像しています。next(sleep_generator)
には一秒後に処理を再開される Future が入っています。
ここで、トリッキーなのは、counter_coroutine_generator
には Generator なのに return
文がある事です。Generator 内の return は raise StopIteration(value)
と同じ意味になります。PEP 380 -- Syntax for Delegating to a Subgenerator これも何となく美しく無い文法です。
Generator を使って Iterator を実装
さて、次に yield
も使わずにさらにプリミティブに Coroutine を実装してみます。Awaitable Objects によると、Iterator を返す __await__
という関数を実装すると Coroutine と互換性のあるオブジェクトを作る事が出来ます。
import asyncio
# 1 から max までの数を一秒ずつ数える。Iterator 版
class Counter():
def __init__(self, max):
self.count = 0
self.max = max
def __iter__(self):
return self
def __await__(self):
return self
def __next__(self):
print('__next__ is called')
self.count += 1
if self.count <= self.max:
print(self.count)
# await をプリミティブに実行してみたつもりだが失敗
sleep_generator = asyncio.sleep(1)
sleep_future = next(sleep_generator)
return sleep_future
else:
raise StopIteration('数え終わりました')
loop = asyncio.get_event_loop()
counter = Counter(3)
print(f'counter is {counter}')
loop.set_debug(True)
result = loop.run_until_complete(counter)
print(result)
loop.close()
実行結果
counter is <__main__.Counter object at 0x100b7fb00>
__next__ is called
1
(ここで処理が止まってしまう。。。)
残念ながら上手く動きませんでした。なんと不思議なことにデバッガで適当にブレークしながらだとちゃんと動きます。なので惜しい所まで行っているようです。興味本位な実用性の無いコードですが、もしも私の間違いを発見したら教えてくださると嬉しいです。
参考