web-dev-qa-db-ja.com

イベントループの外でコルーチンを実行するにはどうすればよいですか?

したがって、通常は、次のようなことを実行してコルーチンの結果を取得します。

_async def coro():
    await asycnio.sleep(3)
    return 'a value'

loop = asyncio.get_event_loop()
value = loop.run_until_complete(coro())
_

好奇心から、イベントループを使用せずにその値を取得する最も簡単な方法は何ですか?

[編集]

さらに簡単な方法は次のとおりです。

_async def coro():
    ...

value = asyncio.run(coro())  # Python 3.7+
_

しかし、_yield from_(またはawait)a coro()[〜#〜] js [ 〜#〜] ?そうでない場合、なぜですか?

13
Charming Robot

ここで2つの質問があります。1つは、コルーチンが「トップレベル」で、またはより具体的には開発環境で待機することです。もう1つは、イベントループなしでコルーチンを実行することです。

最初の質問に関しては、Chrome Canary Dev Tools-イベントループとの独自の統合を介してそれを処理するツールによって可能であるように、これはPythonで確かに可能です。そして実際、IPython 7.0以降はasyncio natrative をサポートしており、予想どおり、トップレベルでawait coro()を使用できます。

2番目の質問に関しては、イベントループなしで単一のコルーチンを駆動するのは簡単ですが、あまり役に立ちません。その理由を調べてみましょう。

コルーチン関数が呼び出されると、コルーチンオブジェクトが返されます。このオブジェクトは、そのsend()メソッドを呼び出すことによって開始および再開されます。コルーチンがsuspendを決定すると(それがawaitsをブロックするため)、send()が返されます。コルーチンがreturnを決定すると(最後に到達したか、明示的なreturnが検出されたため)、StopIteration属性を持つvalue例外が発生します戻り値に設定します。このことを念頭に置くと、単一のコルーチンの最小ドライバーは次のようになります。

_def drive(c):
    while True:
        try:
            c.send(None)
        except StopIteration as e:
            return e.value
_

これは単純なコルーチンに最適です。

_>>> async def pi():
...     return 3.14
... 
>>> drive(pi())
3.14
_

または、もう少し複雑なものでも:

_>>> async def plus(a, b):
...     return a + b
... 
>>> async def pi():
...     val = await plus(3, 0.14)
...     return val
... 
>>> drive(pi())
3.14
_

しかし、まだ何かが欠けています-上記のコルーチンのどれも、それらの実行をsuspendしません。コルーチンが中断すると、他のコルーチンの実行が可能になり、イベントループが多数のコルーチンを一度に実行(表示)できるようになります。たとえば、asyncioにはsleep()コルーチンがあり、待機すると、指定された期間実行が一時停止されます。

_async def wait(s):
    await asyncio.sleep(1)
    return s

>>> asyncio.run(wait("hello world"))
'hello world'      # printed after a 1-second pause
_

ただし、driveはこのコルーチンの実行に失敗しました:

_>>> drive(wait("hello world"))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in drive
  File "<stdin>", line 2, in wait
  File "/usr/lib/python3.7/asyncio/tasks.py", line 564, in sleep
    return await future
RuntimeError: await wasn't used with future
_

何が起こったかというと、sleep()が特別な「未来」オブジェクトを生成することによってイベントループと通信するということです。未来を待っているコルーチンは、未来が設定された後にのみ再開できます。 「本当の」イベントループは、未来が終了するまで他のコルーチンを実行することによってそうします。

これを修正するために、ミニイベントループで動作する独自のsleep実装を作成できます。これを行うには、イテレータを使用してawaitableを実装する必要があります。

_class my_sleep:
    def __init__(self, d):
        self.d = d
    def __await__(self):
        yield 'sleep', self.d
_

コルーチンの呼び出し元には表示されないタプルを生成しますが、drive(イベントループ)に何をすべきかを伝えます。 drivewaitは次のようになります。

_def drive(c):
    while True:
        try:
            susp_val = c.send(None)
            if susp_val is not None and susp_val[0] == 'sleep':
                time.sleep(susp_val[1])
        except StopIteration as e:
            return e.value

async def wait(s):
    await my_sleep(1)
    return s
_

このバージョンでは、waitは期待どおりに機能します。

_>>> drive(wait("hello world"))
'hello world'
_

コルーチンを駆動する唯一の方法はdrive()を呼び出すことであり、これも単一のコルーチンをサポートするため、これはまだあまり役に立ちません。したがって、単にtime.sleep()を呼び出して1日だけ呼び出す同期関数を作成することもできます。コルーチンが非同期プログラミングのユースケースをサポートするには、drive()が次のことを行う必要があります。

  • 複数のコルーチンの実行と一時停止をサポート
  • ドライブループに新しいコルーチンの生成を実装する
  • コルーチンが、ファイル記述子が読み取り可能または書き込み可能になるなどのIO関連のイベントでウェイクアップを登録できるようにします-その間、パフォーマンスを失うことなく複数のそのようなイベントをサポートします

これは、asyncioイベントループが他の多くの機能と共にテーブルにもたらすものです。イベントループを最初から構築することは、デビッドビーズリーによる this talk で実演されており、ライブの聴衆の前で機能的なイベントループを実装しています。

10
user4815162342

少し調べてみたところ、コルーチンをグローバルに実行する最も簡単な解決策がわかったと思います。

もしあなたが>>> dir(coro) Pythonは以下の属性を出力します:

['__await__', '__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'cr_await', 'cr_code', 'cr_frame', 'cr_Origin', 'cr_running', 'send', 'throw']

いくつかの属性が際立っています、つまり:

[
   '__await__',
   'close',
   'cr_await',
   'cr_code',
   'cr_frame',
   'cr_Origin',
   'cr_running',
   'send',
   'throw'
]

yield(yield)は何をしますか? を読んだ後、一般的にジェネレーターがどのように機能するかを読んだ後、sendメソッドがキーでなければならないことを理解しました。

だから私はしようとしました:

>>> the_actual_coro = coro()
<coroutine object coro at 0x7f5afaf55348> 

>>>the_actual_coro.send(None)

そして興味深いエラーが発生しました:

Original exception was:
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
StopIteration: a value

実際に戻り値を例外で返しました!

だから私は非常に基本的なループを考えました、まあ、それはランナーのようなもので、次のように実装できます:

def run(coro):
    try:
        coro.send(None)
    except StopIteration as e:
        return e.value

これで、コルーチンを同期関数で実行できます。グローバルに実行することもできますが、そうすることはお勧めしません。しかし、コルーチンを実行するために使用できる最も簡単で最低のレベルを知ることは興味深いです

>>> run(coro())
'a value'

ただし、これはNoneに待機する必要があるものがある場合にcoroを返します(これは、コルーチンであるということの本質です)。

これはおそらく、イベントループがコルーチン(coro.cr_frame.f_locals)それらを先物に割り当て、それらを別々に処理することによって?私の単純なrun関数は明らかに提供していません。その点で私は間違っているかもしれません。だから私が間違っているなら誰かが私を訂正してください。

3
Charming Robot

コルーチンはイベントループでのみ実行できるため、イベントループを使用せずにコルーチンの値を取得する方法はありません。

ただし、明示的にrun_until_completeに渡さなくても、コルーチンを実行できます。イベントループの実行中に、値が取得されるのを待つだけです。例えば:

import asyncio


async def test():
    await asyncio.sleep(1)
    return 'a value'


async def main():
    res = await test()
    print('got value from test() without passing it to EL explicitly')
    print(res)


if __name__ ==  '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
3