Is it more Pythonic (and / or performant) to use or to prevent coroutines when making coroutine tail calls in Python?

In Python 3.5+, I often find myself in a situation where I have a lot of nested coroutines to call something that is deeply coroutine, where await just goes into the tail call in most functions, like this:

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

Those functions a , b and c can be written as regular functions that return a coroutine, and not as coroutines themselves, as follows:

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

Which way is more Pythonic? Which method is more effective? Which way will be easier for others to support in the future?

Edit - Performance Measurement

As a performance test, I removed the await asyncio.sleep(time) from deep and programmed 1,000,000 iterations of await a(0.1) . On my test system with CPython 3.5.2, the first version took about 2.4 seconds, and the second version took about 1.6 seconds. So it looks like there might be a execution penalty to do all the coroutines, but this, of course, is not the order. Perhaps someone with a lot of experience programming Python code can create the right test and solve the performance problem completely.

+5
source share
2 answers

Use the first one :. You not only explicitly show the places where the code can be paused (where await is placed), but also get all the benefits associated with it, such as tracebacks, which show a useful thread of execution

To see the difference, modify the deep coroutine to cause an error:

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

For the first fragment, you will see this output:

 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 

But only for the second fragment:

 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 

As you can see, the first trace helps you see the "real" (and useful) thread of execution, and the second does not.

The first way to write code is also much better to support: imagine that you immediately realized that b(time) should also contain some asynchronous call, for example, await asyncio.sleep(time) . In the first code snippet this call can be placed directly without any other changes, but the second you will have to rewrite many parts of your code.

+5
source

This is one of the rare cases where "is it Pythonic?" not really an opinion based question. Tail call optimization is officially non-Python:

So let me defend my position (this is what I don't want [exception of tail recursion] in the language). If you want a short answer, it's just unpythonic - BDFL

( see also )

+1
source

Source: https://habr.com/ru/post/1268289/


All Articles