Master Python Coroutines: From Generators to async/await
This article walks through the evolution of Python's coroutine support—from early generator‑based yield/send patterns, through the introduction of @asyncio.coroutine and yield from, to the modern async and await syntax—explaining concepts, execution flow, and providing clear code examples.
1. Generator transformation with yield/send
When a function contains the yield keyword it becomes a generator, which is an iterator that can be iterated with for and can receive external values via the send() method.
<code>def mygen(alist):
while len(alist) > 0:
c = randint(0, len(alist)-1)
yield alist.pop(c)
</code>Calling g = mygen(a) returns a generator object. The first call must be g.send(None) (or next(g) ) to start execution up to the first yield . Each subsequent g.send(value) resumes the generator, assigns the sent value to the variable on the left side of receive = yield value , computes a new value , and pauses again. The execution order after the initial start is receive → yield → pause , so the loop runs steps 3 → 1 → 2 repeatedly.
2. yield from
yield from delegates to another iterable, automatically yielding each of its items. It is essentially a shortcut for:
<code>for item in iterable:
yield item
</code>Example:
<code>def g1():
yield range(5)
def g2():
yield from range(5)
</code>Running g1() yields the range object itself, while g2() yields the numbers 0‑4 individually.
A Fibonacci generator using yield :
<code>def fab(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n += 1
</code>Wrapping a generator with a logging wrapper using yield from :
<code>def f_wrapper(fun_iterable):
print('start')
for item in fun_iterable:
yield item
print('end')
</code>3. @asyncio.coroutine and yield from
Before Python 3.5, asynchronous functions were declared with @asyncio.coroutine and used yield from to await other coroutines such as asyncio.sleep . Example:
<code>@asyncio.coroutine
def smart_fib(n):
index = 0
a, b = 0, 1
while index < n:
sleep_secs = random.uniform(0, 0.2)
yield from asyncio.sleep(sleep_secs)
print('Smart one think {} secs to get {}'.format(sleep_secs, b))
a, b = b, a + b
index += 1
</code>The same pattern is shown for a “stupid” version with a longer sleep interval. Both coroutines are scheduled with an event loop: <code>loop = asyncio.get_event_loop() tasks = [smart_fib(10), stupid_fib(10)] loop.run_until_complete(asyncio.wait(tasks)) loop.close() </code> Note that yield from must receive an iterable (a coroutine is iterable), so using time.sleep raises TypeError: ‘NoneType’ object is not iterable . 4. async and await Python 3.5 introduced async def to define coroutine functions and await to suspend execution until an awaitable completes. Unlike asyncio.coroutine , async cannot turn a generator into a coroutine; the function must contain await expressions. <code>async def mygen(alist): while len(alist) > 0: c = randint(0, len(alist)-1) print(alist.pop(c)) await asyncio.sleep(1) </code> Running the async generator requires an event loop: <code>if __name__ == '__main__': loop = asyncio.get_event_loop() tasks = [mygen(strlist), mygen(intlist)] loop.run_until_complete(asyncio.wait(tasks)) loop.close() </code> This demonstrates alternating execution of the two coroutines.
Python Programming Learning Circle
A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.