Python native coroutines and send()

2020-02-04 10:18发布

问题:

Generator based coroutines have a send() method which allow bidirectional communication between the caller and the callee and resumes a yielded generator coroutine from the caller. This is the functionality that turns generators into coroutines.

While the new native async/await coroutines provide superior support for async I/O, I do not see how to get the equivalent of send() with them. The use of yield in async functions is explicitly forbidden, so native coroutines can return only once using a return statement. Although await expressions bring new values into a coroutine, those values come from callees, not the caller, and the awaited call is evaluated from the beginning each time, not from where it left off.

Is there a way to resume a returned coroutine from where it left off and potentially send in a new value? How can I emulate the techniques in David Beazley's Curious Course on Coroutines and Concurrency using native coroutines?

The general code pattern I have in mind is something like

def myCoroutine():
  ...
  while True:
    ...
    ping = yield(pong)
    ...

and in the caller

while True:
  ...
  buzz = myCoroutineGen.send(bizz)
  ...

Edit

I accepted Kevin's answer but I have noticed that the PEP says

Coroutines are based on generators internally, thus they share the implementation. Similarly to generator objects, coroutines have throw() , send() and close() methods.

...

throw() , send() methods for coroutines are used to push values and raise errors into Future-like objects.

So apparently native coroutines do have a send()? How does it work without yield expression to receive the values inside the coroutine?

回答1:

Is there a way to resume a returned coroutine from where it left off and potentially send in a new value?

No.

async and await are just syntactic sugar for yield from. When a coroutine returns (with the return statement), that's it. The frame is gone. It is not resumable. This is exactly how generators have always worked. For example:

def foo():
    return (yield)

You can do f = foo(); next(f); f.send(5), and you will get back 5. But if you try to f.send() again, it does not work, because you already returned from the frame. f is no longer a live generator.

Now, as for new coroutines, so far as I can tell, it seems yielding and sending is reserved for communication between the event loop and certain basic predicates such as asyncio.sleep(). The coroutines yield asyncio.Future objects up to the event loop, and the event loop sends those same future objects back into the coroutine once the associated operations have been completed (they are typically scheduled via call_soon() and the other event loop methods).

You can yield future objects by awaiting them, but it's not a general-purpose interface like .send() was. It is specifically intended for use by the event loop implementation. If you are not implementing an event loop, you probably do not want to be playing around with this. If you are implementing an event loop, you need to ask yourself why the perfectly good implementations in asyncio are not sufficient for your purposes and explain what specifically you are trying to do before we can help you.

Please note that yield from is not deprecated. If you want coroutines that are not tied to an event loop at all, just use that instead. async and await are specifically designed for asynchronous programming with event loops. If that is not what you are doing, then async and await are the wrong tool to begin with.

One more thing:

The use of yield in async functions is explicitly forbidden, so native coroutines can return only once using a return statement.

await expressions do yield control. await something() is entirely analogous to yield from something(). They just changed the name so it would be more intuitive to people not familiar with generators.


For those of you who actually are interested in implementing your own event loop, here's some example code showing a (very minimal) implementation. This event loop is extremely stripped down, because it is designed to run certain specially-written coroutines synchronously as if they were normal functions. It does not provide the full range of support you would expect from a real BaseEventLoop implementation, and is not safe for use with arbitrary coroutines.

Ordinarily, I would include the code in my answer, rather than linking to it, but there are copyright concerns and it is not critical to the answer itself.