LoginSignup
1
3

More than 5 years have passed since last update.

Python の Generator や Iterator を使って Coroutine を実装 (失敗)

Last updated at Posted at 2019-03-07

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
(ここで処理が止まってしまう。。。)

残念ながら上手く動きませんでした。なんと不思議なことにデバッガで適当にブレークしながらだとちゃんと動きます。なので惜しい所まで行っているようです。興味本位な実用性の無いコードですが、もしも私の間違いを発見したら教えてくださると嬉しいです。

(質問中: https://stackoverflow.com/questions/55039539/can-a-python-coroutine-be-implemented-without-await-or-yield)

参考

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3