[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