Python’s yield statement is a powerful tool that can be used in a variety of contexts, including generator functions, coroutines, and asynchronous programming.
Table of Contents
Generators
yield was introduced in Python 2.2 as a simple way to create generators (PEP 255). Generators are functions that can pause their execution and return a value, then resume when next is called on them again. This “pausing function” functionality of yield enables some very powerful programming concepts in Python.
When a generator function is called, it returns a generator object that can be used to generate the values one at a time. Each time the yield statement is encountered in the generator function, the function is paused and the value following the yield keyword is returned. The next time the generator is called, execution resumes after the last yield statement.
Here’s an example of a simple generator function:
def double_gen(n):
for x in range(n):
a = 2 * x
yield a
print(f"last value: {a}", end=', ')
gen = double_gen(3)
print(f"curr value: {next(gen)}")
# curr value: 0
print(f"curr value: {next(gen)}")
# last value: 0, curr value: 2
print(f"curr value: {next(gen)}")
# last value: 2, curr value: 4
print(f"curr value: {next(gen)}")
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# StopIteration
# last value: 4,
- In this example, we define a generator function called
double_genthat generates the doubles of the numbers from0tonusing theyieldkeyword. - We then create a generator object called
genby calling the function. Whennext()is first invoked, it setsato0and yieldsaback to its caller, where we printcurr value: 0. - When
double_genis resumed,double_gencontinues after theyieldwith local state intact, where we printlast value: 0,, anddouble_genloops back to theyield, yielding2(a = 2 * 1) to its invoker. - And so on, until the generator function runs out, at this point a
StopIterationexception is raised.
Generator Expressions
In addition to using generator functions, generators can also be created using generator expressions (PEP 289), which are similar to list comprehensions and return an generator instead of a list.
gen = (x**2 for x in range(10))
# print(gen)
# <generator object <genexpr> at 0x7fbd75d43ae0>
Generator expressions are essentially syntactic sugar for generators. They provide a concise way to create simple generators, but ultimately they compile down to the same generator objects that can be created using def. The above example is equivalent to:
def gen():
for x in range(10):
yield x**2
Context Managers
Context managers are objects that define a context in which a set of operations can be performed. Context managers can be defined by generators using the @contextmanager decorator, in which case, yield keyword used in generator acts more like a separator between code entering and exiting the context manager.
from contextlib import contextmanager
@contextmanager
def file_opener(filename, mode):
try:
# entering context manager
f = open(filename, mode)
yield f
# exiting context manager
finally:
f.close()
- In this example, we have defined a
file_openercontext manager using the@contextmanagerdecorator. Thefile_openerfunction is a generator function that yields a file object, which can be used by the calling code to read or write data to the file. - The
tryblock in thefile_openerfunction opens the file with the specified filename and mode. Theyieldstatement produces the file object and temporarily suspends the generator function, allowing the calling code to resume execution. The calling code can use the file object to read or write data to the file. - The
finallyblock in thefile_openerfunction ensures that the file is closed properly when the context is exited, even if an exception occurs during execution.
Here’s an example of how you can use the file_opener context manager:
with file_opener("hello.txt", "w") as f:
f.write("Hello, world!")
yield expressions
Python 2.5 (PEP 342) introduced methods and adjustments to use generators to implement coroutines and other forms of co-operative multitasking. The most interesting features were yield expression and send() method.
For example:
def guess(n):
hint = None
while True:
x = yield hint
if x > n:
hint = 'too big'
elif x < n:
hint = 'too small'
else:
hint = 'bingo'
>>> game = guess(5)
>>> game.send(None)
>>> game.send(1)
'too small'
>>> game.send(10)
'too big'
>>> game.send(5)
'bingo'
- In this example, first we call the generator function to define a generator
game = guess(5)to play a guess number game with target number 5. - And then we call
game.send(None)just likenext()to advance the generatorguessto theyieldwhich will pause the execution and yield aNonevalue. - After we call
game.send(1), the generatorguessreceives a value viax = yield(think it asx = f(), where the return value off()is1which is sent bygame.send(1)) and resumes its execution (executingif-elif-elseblock) untilyield hintand returns local statehintto its callergame.send(1)(likenext()), wheregame.send(1)returns'too small'.
The send() method allows you to resume the execution of the generator or coroutine at the point where it left off, with a new value that is sent back into the generator or coroutine.
Generator-based Coroutines
yield later gained additional power with the introduction of yield from (introduced in Python 3.3, PEP 380). yield from allowed a generator to delegate to another generator or coroutine.
For example:
def double_gen(n):
for x in range(n):
yield 2 * x
def triple_gen(n):
for x in range(n):
yield 3 * x
def delegate_gen(n):
double_gen(n)
triple_gen(n)
for x in delegate_gen(3):
print(x)
# TypeError: 'NoneType' object is not iterable
- When we run the above code, we will get
TypeErrorbecause thedelegate_genfunction is just a regular function in which only two generator objects are created. - Because when we call the generator functions, we simply get the generator object, not exactly call the functions in the normal way and we need to explicitly iterate generator again and again to re-yield values that it produces.
- In this example
delegate_gendoesn’t have anyieldstatements so it cannot be used as generator in theforloop. And this regular function will returnNoneas default, that’s why we getTypeError: 'NoneType' object is not iterable.
With yield from expression (the “delegating generator”), we can make delegate_gen a generator function.
def double_gen(n):
for x in range(n):
yield 2 * x
def triple_gen(n):
for x in range(n):
yield 3 * x
def delegate_gen(n):
yield from double_gen(n)
yield from triple_gen(n)
for x in delegate_gen(3):
print(x, end=' ')
# prints: 0 2 4 0 3 6
yield from simply provides a clean syntax to delegate control between coroutines, passing values back and forth. This enables powerful patterns like coroutine pipelines, trees, and more complex asynchronous algorithms built from simpler components.
With async def keyword we can define coroutines from generators:
import asyncio
async def main():
for x in delegate_gen(3):
print(x, end=' ')
asyncio.run(main())
# 0 2 4 0 3 6
# main() is a coroutine
Caveats. According to the documentation Generator-based Coroutines: Support for generator-based coroutines is deprecated and is scheduled for removal in Python 3.10. Use async def instead.
Generator-based coroutines predate async/await syntax. They are Python generators that use yield from expressions to await on Futures and other coroutines.
Generator-based coroutines should be decorated with @asyncio.coroutine, although this is not enforced. This decorator enables legacy generator-based coroutines to be compatible with async/await code. This decorator should not be used for async def coroutines.
@asyncio.coroutine
def old_style_coroutine():
yield from asyncio.sleep(1)
async def main():
await old_style_coroutine()
Native Coroutines
There are a number of shortcomings to implement coroutines via generators because with yield or yield from. For example, it is sometimes confusing to distinguish coroutines from regular generators. In Python 3.5 (PEP 492) native coroutines and the associated new syntax features such as async and await were introduced.
async def
We can define a native coroutine with async def:
async def coro(n):
pass
Some key properties:
- Functions defined with
async defare always coroutines, even if they do not containawaitexpressions. - It is a
SyntaxErrorto haveyieldoryield fromexpressions in anasyncfunction. (yieldcan be used inasyncfunction, see PEP 525) - Regular generators, when called, return a generator object; similarly, coroutines return a coroutine object.
StopIterationexceptions are not propagated out of coroutines, and are replaced with aRuntimeError.CO_COROUTINEis used to mark native coroutines andCO_ITERABLE_COROUTINEis used to make generator-based coroutines compatible with native coroutines.
await
await uses the yield from implementation with an extra step of validating its argument. await only accepts an awaitable. There are three main types of awaitable objects: coroutines, Tasks, and Futures.
Asynchronous Generators
In Python 3.6 (PEP 525), the concept of asynchronous generators were introduced.
The asynchronous generator object is modeled after the standard Python generator object. Essentially, the behaviour of asynchronous generators is designed to replicate the behaviour of synchronous generators, with the only difference in that the API is asynchronous.
Asynchronous generators can be defined with async def and yield.
import asyncio
# an asynchronous generator function
async def double_coro(n):
for x in range(n):
yield 2 * x
await asyncio.sleep(1)
Asynchronous generators require an event loop to run and finalize them because they are meant to be used from coroutines and whereas an event loop or a scheduler is required to run coroutines. We can wrap it in a coroutine and run it with asyncio.run.
async def main():
async for x in double_coro(3):
print(x, end=' ')
asyncio.run(main())
# prints: 0 2 4
For an asynchronous generator object agen, the following methods and properties are defined:
agen.__aiter__(): Returnsagen.agen.__anext__(): Returns an awaitable, that performs one asynchronous generator iteration when awaited.agen.asend(val): Returns an awaitable, that pushes thevalobject in the agen generator. When theagenhas not yet been iterated,valmust beNone.
Asyncio
asyncio is a Python library for asynchronous programming, introduced in Python 3.4 (PEP 3156). It provides a way to write asynchronous, non-blocking code using coroutines, tasks, and event loops.
When the asyncio module was first released, it did not support the async and await syntax. Instead, it used the yield from syntax to define coroutines. Later, when the async and await syntax were introduced in Python 3.5, the asyncio module was updated to support this new syntax.
However, to ensure backwards compatibility with legacy code that was using the old yield from syntax, the asyncio.coroutine decorator function was introduced. Any legacy code that had a function that needed to be run concurrently (i.e. awaited) had to use this decorator function to make it compatible with the new async and await syntax.
In short, the asyncio.coroutine decorator function was a temporary solution to ensure that existing code that used the old yield from syntax could still be used with the new async and await syntax. It allowed developers to gradually transition their code to the new syntax. This feature has been deprecated since Python 3.8.
Key features
asyncio provides a number of key features, including:
- Coroutines: Coroutines can be defined using the
async defsyntax, and can use theawaitkeyword to pause their execution until an asynchronous operation completes. Coroutines (and tasks) can only run when the event loop is running. - Tasks: A Task is an object that manages an independently running coroutine. The Task interface is the same as the Future interface, and in fact Task is a subclass of Future. Tasks are also useful for interoperating between coroutines and callback-based frameworks like Twisted. After converting a coroutine into a Task, callbacks can be added to the Task. To convert a coroutine into a task, call the coroutine function and pass the resulting coroutine object to the
loop.create_task()method. You may also useasyncio.ensure_future()for this purpose. - Event Loop: The event loop is the core of every asyncio application. Event loops run asynchronous tasks and callbacks, perform network IO operations, and run subprocesses. To get the event loop for current context, use
get_event_loop(). - Futures:
asynciouses futures to represent the result of an asynchronous operation that has not yet completed. Futures can be awaited like coroutines, and can be used to retrieve the result of an asynchronous operation once it completes.
Alternatives
Asyncio is just one of several Python libraries for asynchronous programming, each with its own strengths and weaknesses. Here’s some of the other popular libraries for asynchronous programming in Python:
- Twisted: Twisted is a Python-based open-source network framework that enables the creation of SMTP, HTTP, proxy, and SSH servers (among other things) with ease. It operates asynchronously and in an event-driven manner, enabling applications to react to various network connections without relying on conventional threading models.
- Tornado: Tornado is an asynchronous networking library and web framework for Python. Unlike many other Python web frameworks, Tornado doesn’t rely on WSGI and typically only requires one thread per process to run.
- Trio: Trio is a contemporary Python library that facilitates the creation of asynchronous applications. These are programs that perform multiple tasks simultaneously by parallelizing I/O operations, such as a web spider that retrieves numerous pages concurrently or a web server managing several simultaneous downloads.
- Curio: Curio is a library for concurrent system programming in Python that operates using coroutines. It offers common programming abstractions like tasks, sockets, files, locks, and queues. It is a small, fast, and enjoyable library that should feel familiar to users.
Related References
- PEP 255 – Simple Generators
- PEP 289 – Generator Expressions
- PEP 342 – Coroutines via Enhanced Generators
- PEP 380 – Syntax for Delegating to a Subgenerator
- PEP 3156 – Asynchronous IO Support Rebooted: the “asyncio” Module
- PEP 492 – Coroutines with async and await syntax
- PEP 525 – Asynchronous Generators
- Coroutines and Tasks
- From yield to async/await
- Python 101: iterators, generators, coroutines
- Asynchronous Context Managers and Asynchronous Iterators