[Python-Dev] PEP 550 v4

Elvis Pranskevichus elprans at gmail.com
Sat Aug 26 10:58:33 EDT 2017


On Saturday, August 26, 2017 2:34:29 AM EDT Nathaniel Smith wrote:
> On Fri, Aug 25, 2017 at 3:32 PM, Yury Selivanov 
<yselivanov.ml at gmail.com> wrote:
> > Coroutines and Asynchronous Tasks
> > ---------------------------------
> > 
> > In coroutines, like in generators, context variable changes are
> > local> 
> > and are not visible to the caller::
> >     import asyncio
> >     
> >     var = new_context_var()
> >     
> >     async def sub():
> >         assert var.lookup() == 'main'
> >         var.set('sub')
> >         assert var.lookup() == 'sub'
> >     
> >     async def main():
> >         var.set('main')
> >         await sub()
> >         assert var.lookup() == 'main'
> >     
> >     loop = asyncio.get_event_loop()
> >     loop.run_until_complete(main())
> 
> I think this change is a bad idea. I think that generally, an async
> call like 'await async_sub()' should have the equivalent semantics
> to a synchronous call like 'sync_sub()', except for the part where
> the former is able to contain yields. Giving every coroutine an LC
> breaks that equivalence. It also makes it so in async code, you
> can't necessarily refactor by moving code in and out of
> subroutines. Like, if we inline 'sub' into 'main', that shouldn't
> change the semantics, but...

If we could easily, we'd given each _normal function_ its own logical 
context as well.

What we are talking about here is variable scope leaking up the call 
stack.  I think this is a bad pattern.  For decimal context-like uses 
of the EC you should always use a context manager.  For uses like Web 
request locals, you always have a top function that sets the context 
vars.

> 
> I think I see the motivation: you want to make
> 
>    await sub()
> 
> and
> 
>    await ensure_future(sub())
> 
> have the same semantics, right? And the latter has to create a Task

What we want is for `await sub()` to be equivalent to 
`await asyncio.wait_for(sub())` and to `await asyncio.gather(sub())`.

Imagine we allow context var changes to leak out of `async def`.  It's 
easy to write code that relies on this:

async def init():
	var.set('foo')

async def main():
	await init()
	assert var.lookup() == 'foo'

If we change `await init()` to `await asyncio.wait_for(init())`, the 
code will break (and in real world, possibly very subtly).

> It also adds non-trivial overhead, because now lookup() is O(depth
> of async callstack), instead of O(depth of (async) generator
> nesting), which is generally much smaller.

You would hit cache in lookup() most of the time.


                                Elvis


More information about the Python-Dev mailing list