通常,您通过执行以下操作来获取协程的结果:
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+
但是有什么方法可以像 JS 那样全局地yield from
(或await
)coro()
?如果不是,为什么?
最佳答案
这里有两个问题:一个是关于“在顶层”或更具体地在开发环境中等待协程。另一个是关于在没有事件循环的情况下运行协程。
关于第一个问题,这在 Python 中当然是可能的,就像在 Chrome Canary Dev Tools 中是可能的一样 - 通过工具通过自身与事件循环的集成来处理它。事实上,IPython 7.0 及更高版本支持 asyncio natively并且您可以按预期在顶层使用 await coro()
。
关于第二个问题,没有事件循环驱动单个协程很容易,但是用处不大。让我们来看看原因。
当协程函数被调用时,它返回一个协程对象。该对象通过调用其 send()
方法启动和恢复。当协程决定暂停(因为它await
是阻塞的东西)时,send()
将返回。当协程决定返回时(因为它已经到达终点或因为它遇到了明确的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
但是仍然缺少一些东西——上述协程都没有暂停它们的执行。当一个协程挂起时,它允许其他协程运行,这使得事件循环能够(看起来)一次执行许多协程。例如,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()
通过产生一个特殊的“ future ”对象与事件循环通信。等待 future 的协程只能在未来设定后恢复。 “真正的”事件循环将通过运行其他协程来实现,直到 future 完成。
为了解决这个问题,我们可以编写自己的 sleep
实现,与我们的迷你事件循环一起使用。为此,我们需要使用迭代器来实现等待:
class my_sleep:
def __init__(self, d):
self.d = d
def __await__(self):
yield 'sleep', self.d
我们产生一个元组,协程调用者看不到它,但会告诉 drive
(我们的事件循环)要做什么。 drive
和 wait
现在看起来像这样:
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()
并调用它一天。为了让我们的协同程序支持异步编程的用例,drive()
需要:
- 支持多个协程的运行和挂起
- 在驱动循环中实现新协程的生成
- 允许协程在 IO 相关事件上注册唤醒,例如文件描述符变为可读或可写 - 同时支持多个此类事件而不会损失性能
这就是 asyncio 事件循环以及许多其他功能带来的好处。 this talk 中出色地演示了从头开始构建事件循环。由 David Beazley 编写,他在现场观众面前实现了一个功能性事件循环。
关于python - 如何在事件循环之外运行协程?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52783605/