What is yield from? Python generators

chemistry programming yield from generators python featured image

Have you ever seen the term yield from in some Python code and wondered what it was? In this article we will delve even deeper into generators and understand, with examples, what yield from means and how we can use it to make our code even more efficient.

In the article about generators we saw that the origin of generators was in the context of coroutines, but that they are applicable in several other contexts, so that knowledge about coroutines is not necessary to apply them. The yield from was formalized in PEP 380, still in a very technical way, aimed at this specific context. But let’s take excerpts from this PEP and bring them closer to simple examples.

The PEP 380 abstract brings the following text:

A syntax is proposed for a generator to delegate part of its operations to another generator. This allows a section of code containing ‘yield’ to be factored out and placed in another generator. Additionally, the subgenerator is allowed to return with a value, and the value is made available to the delegating generator. The new syntax also opens up some opportunities for optimisation when one generator re-yields values produced by another.

The key point is the written in the first sentence: a generator delegating part of its operations to another generator.

Another quote is in the language expressions documentation, where we have:

When yield from is used, it treats the supplied expression as a subiterator. (…) the supplied expression must be an iterable.

That is, we can convert an iterable into an iterator and generate from it. Interesting. And, if you need to remember the concepts, see this article on iterators and iterables with several examples.

Let’s see how to apply the presented in some simple examples.

Simple examples

Recently I wrote about infinite sequences and in that article we saw the use of the islice method. Let’s use all the knowledge acquired here.

Consuming from two generators

Consider that we have two generators of infinite sequences, one for positive even integers and the other for positive odd integers:

from itertools import islice


def positive_even_numbers():
    value = 0
    while True:
        yield value
        value += 2


def positive_odd_numbers():
    value = 1
    while True:
        yield value
        value += 2

Suppose now that we want a generator of positive integers, without discriminating whether it is even or odd. We can take advantage of the generators already created:

def positive_integers():
    for pair, odd in zip(positive_even_numbers(), positive_odd_numbers()):
        yield pair
        yield odd

We can check the first 10 yielded values:

gen_int = positive_integers()
tuple(islice(gen_int, 10))
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

It worked… but how? Let’s start by looking at the documentation of the zip function:

help(zip)
Help on class zip in module builtins:

class zip(object)
 |  zip(*iterables, strict=False) --> Yield tuples until an input is exhausted.
 |
 |     >>> list(zip('abcdefg', range(3), range(4)))
 |     [('a', 0, 0), ('b', 1, 1), ('c', 2, 2)]
 |
 |  The zip object yields n-length tuples, where n is the number of iterables
 |  passed as positional arguments to zip().  The i-th element in every tuple
 |  comes from the i-th iterable argument to zip().  This continues until the
 |  shortest argument is exhausted.
 |
 |  If strict is true and one of the arguments is exhausted before the others,
 |  raise a ValueError.
 |
 |  Methods defined here:
 |
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |
 |  __iter__(self, /)
 |      Implement iter(self).
 |
 |  __next__(self, /)
 |      Implement next(self).
 |
 |  __reduce__(...)
 |      Return state information for pickling.
 |
 |  __setstate__(...)
 |      Set state information for unpickling.
 |
 |  ----------------------------------------------------------------------
 |  Static methods defined here:
 |
 |  __new__(*args, **kwargs) from builtins.type
 |      Create and return a new object.  See help(type) for accurate signature.

Note that zip receives iterables and yields tuples until these iterables are consumed. So:

zip(positive_even_numbers(), positive_odd_numbers())
next(zip(positive_even_numbers(), positive_odd_numbers()))
(0, 1)

The 5 first pairs yielded are:

tuple(islice(zip(positive_even_numbers(), positive_odd_numbers()), 5))
((0, 1), (2, 3), (4, 5), (6, 7), (8, 9))

So, basically the for loop in the generator body goes through each item of each tuple, making it available (yield) for calls to the generator.

Moving on, if the zip function yields tuples, and tuples are iterables, we can pass these tuples to yield from according to the documentation seen earlier. Thus, we can redefine our generator as:

def positive_integers():
    for tuple in zip(positive_even_numbers(), positive_odd_numbers()):
        yield from tuple
gen_int = positive_integers()
tuple(islice(gen_int, 10))
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

Simple and succinct.

Consuming from another generator

Moving forward, let’s better understand the part of the PEP that talks about delegating part of the operations to a generator. Let’s create a generator of positive integers divisible by 10. We know that to be divisible by 10, a number must end in 0 and, therefore, it is an even number. So it makes sense to use the already created generator of even numbers and generate only those divisible by 10 on demand:

def positive_integers_divisible_by_ten():
    even = positive_even_numbers()
    yield from (e for e in even if (e % 10 == 0))

See that we created a generator expression and we are yielding from it with yield from.

gen_ten = positive_integers_divisible_by_ten()
tuple(islice(gen_ten, 10))
(0, 10, 20, 30, 40, 50, 60, 70, 80, 90)

“Flattening” lists of lists

To illustrate the concept, let’s consider that you have a list of lists containing numbers and you would like to yield these numbers as if they belonged to only one list. This process is called flatten, something like flattening the sublists in the main list.

list_of_lists = [[1, 2, 3], [4, 5, 6], [7], [8, 9]]

A way to write a generator would be:

def flatten(list_of_lists):
    for list_ in list_of_lists:
        for item in list_:
            yield item
gen = flatten(list_of_lists)
tuple(gen)
(1, 2, 3, 4, 5, 6, 7, 8, 9)

Now, each inner list is an iterable and, therefore, we can pass it directly to yield from:

def flatten(list_of_lists):
    for list_ in list_of_lists:
        yield from list_
gen = flatten(list_of_lists)
tuple(gen)
(1, 2, 3, 4, 5, 6, 7, 8, 9)

This flattening has several real applications and this approach is not necessarily the most efficient, read more here. But it is a good way to see more applications of yield from.

Conclusion

Did you enjoy learning more about generators? It is a very efficient way to deal with various situations where it is not possible, from a resource perspective, to store all values previously.

Did you like this article? It is part of the Python Drops, a set of shorter posts focused on fundamentals talking about some aspects of the Python language and programming in general. You can read more of these articles by searching for the tag “drops” here on the site. Until next time!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top