在Python中进行协程尾调用时,使用或避免协同程序是否更具Pythonic(和/或高性能)?

Joh*_*eks 6 python coroutine python-asyncio

在Python 3.5+中,我常常会遇到这样一种情况,即我有很多嵌套协同程序,只是为了调用一个深层次的协程,await在大多数函数中只有一个尾调用,如下所示:

import asyncio

async def deep(time):
    await asyncio.sleep(time)
    return time

async def c(time):
    time *= 2
    return await deep(time)

async def b(time):
    time *= 2
    return await c(time)

async def a(time):
    time *= 2
    return await b(time)

async def test():
    print(await a(0.1))

loop = asyncio.get_event_loop()
loop.run_until_complete(test())
loop.close()
Run Code Online (Sandbox Code Playgroud)

这些功能a,b以及c可以写成返回协程,定期的功能,而不是为自己的协同程序,如下:

import asyncio

async def deep(time):
    await asyncio.sleep(time)
    return time

def c(time):
    time *= 2
    return deep(time)

def b(time):
    time *= 2
    return c(time)

def a(time):
    time *= 2
    return b(time)

async def test():
    print(await a(0.1))

loop = asyncio.get_event_loop()
loop.run_until_complete(test())
loop.close()
Run Code Online (Sandbox Code Playgroud)

哪种方式更像Pythonic?哪种方式更高效?以后哪种方式更容易维护?

编辑 - 绩效评估

作为性能测试,我await asyncio.sleep(time)deep1,000,000次迭代中删除了该行并计时await a(0.1).在我的CPython 3.5.2测试系统上,第一个版本大约需要2.4秒,第二个版本需要大约1.6秒.所以看起来制作所有协同程序可能会有性能损失,但它肯定不是一个数量级.也许拥有更多Python代码分析经验的人可以创建一个合适的基准并最终解决性能问题.

Mik*_*mov 5

使用第一个:您不仅可以显示代码可以挂起的位置await(放置位置),还可以获得所有相关的好处,例如显示有用执行流程的回溯.

要查看差异,请更改您的deep协程以抛出一些错误:

async def deep(time):
    await asyncio.sleep(time)
    raise ValueError('some error happened')
    return time
Run Code Online (Sandbox Code Playgroud)

对于第一个片段,您将看到此输出:

Traceback (most recent call last):
  File ".\tmp.py", line 116, in <module>
    loop.run_until_complete(test())
  File ".\Python36\lib\asyncio\base_events.py", line 466, in run_until_complete
    return future.result()
  File ".\tmp.py", line 113, in test
    print(await a(0.1))
  File ".\tmp.py", line 110, in a
    return await b(time)
  File ".\tmp.py", line 106, in b
    return await c(time)
  File ".\tmp.py", line 102, in c
    return await deep(time)
  File ".\tmp.py", line 97, in deep
    raise ValueError('some error happened')
ValueError: some error happened
Run Code Online (Sandbox Code Playgroud)

但仅限第二个片段:

Traceback (most recent call last):
  File ".\tmp.py", line 149, in <module>
    loop.run_until_complete(test())
  File ".\Python36\lib\asyncio\base_events.py", line 466, in run_until_complete
    return future.result()
  File ".\tmp.py", line 146, in test
    print(await a(0.1))
  File ".\tmp.py", line 130, in deep
    raise ValueError('some error happened')
ValueError: some error happened
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,第一个回溯可以帮助您查看"真实"(和有用的)执行流程,而第二个则不会.

编写代码的第一种方法也更好维护:想象一下你曾经理解过b(time)还应该包含一些异步调用,比如await asyncio.sleep(time).在第一个代码段中,可以直接放置此调用而无需任何其他更改,但在第二个代码中,您将不得不重写代码的许多部分.