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

时间:2017-05-28 01:46:06

标签: python python-asyncio coroutine

在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()

这些函数abc可以编写为返回协程的常规函数​​,而不是协程本身,如下所示:

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()

哪种方式更像Pythonic?哪种方式更高效?其他人在将来哪种方式更容易维护?

编辑 - 绩效衡量

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

2 个答案:

答案 0 :(得分:5)

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

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

async def deep(time):
    await asyncio.sleep(time)
    raise ValueError('some error happened')
    return time

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

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

但仅限第二个片段:

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

正如您所看到的,首先追溯可以帮助您查看&#34;真实&#34; (并且有用)执行流程,而第二个不是。

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

答案 1 :(得分:1)

这是一种罕见的情况,其中&#34;是Pythonic?&#34;实际上不是基于意见的问题。尾调用优化是官方的非Pythonic:

  

所以让我捍卫自己的立场(这是我不希望语​​言中的[尾递归消除])。如果你想要一个简短的答案,它只是unpythonic - the BDFL

see also