在 Python 中进行协程尾调用时,使用协程还是避免协程更 Pythonic(and/or 性能)?
Is it more Pythonic (and/or performant) to use or to avoid coroutines when making coroutine tail calls in Python?
在 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()
这些函数 a
、b
和 c
可以写成 return 协程的常规函数,而不是协程本身,如下所示:
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?哪种方式性能更高?哪种方式以后别人维护起来更方便?
编辑 — 绩效衡量
作为性能测试,我从 deep
中删除了 await asyncio.sleep(time)
行并计时了 await a(0.1)
的 1,000,000 次迭代。在我使用 CPython 3.5.2 的测试系统上,第一个版本花费了大约 2.4 秒,第二个版本花费了大约 1.6 秒。所以看起来让所有东西都成为协程可能会有性能损失,但这肯定不是一个数量级。也许具有更多分析 Python 代码经验的人可以创建适当的基准并明确解决性能问题。
使用第一个:您不仅可以明确显示可以暂停代码的位置(放置 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
如您所见,第一个回溯可以帮助您查看 "real"(并且很有帮助)执行流程,而第二个则不能。
第一种编写代码的方式也更易于维护:假设您曾经理解 b(time)
还应该包含一些异步调用,例如 await asyncio.sleep(time)
。在第一个片段中,可以直接放置此调用而无需任何其他更改,但在第二个片段中,您将不得不重写代码的许多部分。
这是 "is it Pythonic?" 实际上 不是 意见型问题的罕见情况之一。尾调用优化正式非 Pythonic:
So let me defend my position (which is that I don't want [tail recursion elimination] in the language). If you want a short answer, it's simply unpythonic - the BDFL
(see also)
在 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()
这些函数 a
、b
和 c
可以写成 return 协程的常规函数,而不是协程本身,如下所示:
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?哪种方式性能更高?哪种方式以后别人维护起来更方便?
编辑 — 绩效衡量
作为性能测试,我从 deep
中删除了 await asyncio.sleep(time)
行并计时了 await a(0.1)
的 1,000,000 次迭代。在我使用 CPython 3.5.2 的测试系统上,第一个版本花费了大约 2.4 秒,第二个版本花费了大约 1.6 秒。所以看起来让所有东西都成为协程可能会有性能损失,但这肯定不是一个数量级。也许具有更多分析 Python 代码经验的人可以创建适当的基准并明确解决性能问题。
使用第一个:您不仅可以明确显示可以暂停代码的位置(放置 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
如您所见,第一个回溯可以帮助您查看 "real"(并且很有帮助)执行流程,而第二个则不能。
第一种编写代码的方式也更易于维护:假设您曾经理解 b(time)
还应该包含一些异步调用,例如 await asyncio.sleep(time)
。在第一个片段中,可以直接放置此调用而无需任何其他更改,但在第二个片段中,您将不得不重写代码的许多部分。
这是 "is it Pythonic?" 实际上 不是 意见型问题的罕见情况之一。尾调用优化正式非 Pythonic:
So let me defend my position (which is that I don't want [tail recursion elimination] in the language). If you want a short answer, it's simply unpythonic - the BDFL
(see also)