问题
I'm transitioning from old-style coroutines (where 'yield' returns a value supplied by 'send', but which are otherwise essentially generators) to new-style coroutines with 'async def' and 'await'. There are a couple of things that really puzzle me.
Consider the following old-style coroutine that computes the running average of numbers supplied to it by 'send', at each point returning the mean-so-far. (This example is from Chapter 16 of Fluent Python by Luciano Ramalho.)
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total/count
If I now create and prime a coroutine object, I can send it numbers and it will return the running average:
>>> coro_avg = averager()
>>> next(coro_avg)
>>> coro_avg.send(10)
10.0
>>> coro_avg.send(30)
20.0
>>> coro_avg.send(5)
15.0
...and so forth. The question is, how would such a coroutine be written with async/await? There are three points that confuse me. Do I understand them correctly?
1) In the old style, anybody can send numbers to the same instance of the averager. I can pass around the value coro_avg above and every time .send(N) is called, no matter from where, N is added to the same running total. With async/await, however, there's no way to "send in a value". Each time you 'await' a coroutine you await a new instance with its own context, its own variable values.
2) It seems that the only way for an 'async def' coroutine to hand a value back to the thing awaiting it is to 'return' and hence lose context. You can't call 'yield' from inside an 'async def' coroutine (or rather if you do you've created an async generator which can't be used with await). So an 'async def' coroutine can't compute a value and hand it out while maintaining context, as averager does.
3) Almost the same as (1): When a coroutine calls 'await' it waits for a single, specific awaitable, namely the argument to await. This is very unlike old-style coroutines, which give up control and sit around waiting for anyone to send something to them.
I realize that the new coroutines are a distinct coding paradigm from the old ones: They're used with event loops, and you use data structures like queues to have the coroutine emit a value without returning and losing context. It's kind of unfortunate and somewhat confusing that new and old share the same name---coroutine---given that their call/return protocols are so different.
回答1:
It is possible, and perhaps instructive, to relate the two models pretty directly. Modern coroutines are actually implemented, like the old, in terms of the (generalized) iterator protocol. The difference is that return values from the iterator are automatically propagated upwards through any number of coroutine callers (via an implicit yield from
), whereas actual return values are packaged into StopIteration
exceptions.
The purpose of this choreography is to inform the driver (the presumed “event loop”) of the conditions under which a coroutine may be resumed. That driver may resume the coroutine from an unrelated stack frame and may send data back into the execution—via the awaited object, since it is the only channel known to the driver—again, much as send
communicates transparently through a yield from
.
An example of such bidirectional communication:
class Send:
def __call__(self,x): self.value=x
def __await__(self):
yield self # same object for awaiter and driver
raise StopIteration(self.value)
async def add(x):
return await Send()+x
def plus(a,b): # a driver
c=add(b)
# Equivalent to next(c.__await__())
c.send(None)(a)
try: c.send(None)
except StopIteration as si: return si.value
raise RuntimeError("Didn't resume/finish")
A real driver would of course decide to call the result of send
only after recognizing it as a Send
.
Practically speaking, you don’t want to drive modern coroutines yourself; they are syntax optimized for exactly the opposite approach. It would, however, be straightforward to use a queue to handle one direction of the communication (as you already noted):
async def avg(q):
n=s=0
while True:
x=await q.get()
if x is None: break
n+=1; s+=x
yield s/n
async def test():
q=asyncio.Queue()
i=iter([10,30,5])
await q.put(next(i))
async for a in avg(q):
print(a)
await q.put(next(i,None))
Providing values that way is a bit painful, but it’s easy if they’re coming from another Queue
or so.
来源:https://stackoverflow.com/questions/57896438/whats-the-difference-between-the-call-return-protocol-of-oldstyle-and-newstyle