Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In practice, what are the main uses for the new "yield from" syntax in Python 3.3?

Tags:

python

yield

I'm having a hard time wrapping my brain around PEP 380.

  1. What are the situations where yield from is useful?
  2. What is the classic use case?
  3. Why is it compared to micro-threads?

So far I have used generators, but never really used coroutines (introduced by PEP-342). Despite some similarities, generators and coroutines are basically two different concepts. Understanding coroutines (not only generators) is the key to understanding the new syntax.

IMHO coroutines are the most obscure Python feature, most books make it look useless and uninteresting.


Thanks for the great answers, but special thanks to agf and his comment linking to David Beazley presentations.

like image 892
Paulo Scardine Avatar asked Mar 14 '12 19:03

Paulo Scardine


People also ask

What is the use of yield in Python?

Yield is a keyword in Python that is used to return from a function without destroying the states of its local variable and when the function is called, the execution starts from the last yield statement. Any function that contains a yield keyword is termed a generator. Hence, yield is what makes a generator.

Which type of function in Python generates a value by the help of yield keyword?

A generator function is defined just like a normal function, but whenever it needs to generate a value, it does so with the yield keyword rather than return. If the body of a def contains yield, the function automatically becomes a generator function.

What is the return type of yield in Python?

The yield keyword in python works like a return with the only difference is that instead of returning a value, it gives back a generator function to the caller. A generator is a special type of iterator that, once used, will not be available again. The values are not stored in memory and are only available when called.

Can I yield two values Python?

Conclusion. Like other programming languages, Python can return a single value, but in this, we can use yield statements to return more than one value for the function. The function that uses the yield keyword is known as a generator function.


2 Answers

Let's get one thing out of the way first. The explanation that yield from g is equivalent to for v in g: yield v does not even begin to do justice to what yield from is all about. Because, let's face it, if all yield from does is expand the for loop, then it does not warrant adding yield from to the language and preclude a whole bunch of new features from being implemented in Python 2.x.

What yield from does is it establishes a transparent bidirectional connection between the caller and the sub-generator:

  • The connection is "transparent" in the sense that it will propagate everything correctly too, not just the elements being generated (e.g. exceptions are propagated).

  • The connection is "bidirectional" in the sense that data can be both sent from and to a generator.

(If we were talking about TCP, yield from g might mean "now temporarily disconnect my client's socket and reconnect it to this other server socket".)

BTW, if you are not sure what sending data to a generator even means, you need to drop everything and read about coroutines first—they're very useful (contrast them with subroutines), but unfortunately lesser-known in Python. Dave Beazley's Curious Course on Coroutines is an excellent start. Read slides 24-33 for a quick primer.

Reading data from a generator using yield from

def reader():     """A generator that fakes a read from a file, socket, etc."""     for i in range(4):         yield '<< %s' % i  def reader_wrapper(g):     # Manually iterate over data produced by reader     for v in g:         yield v  wrap = reader_wrapper(reader()) for i in wrap:     print(i)  # Result << 0 << 1 << 2 << 3 

Instead of manually iterating over reader(), we can just yield from it.

def reader_wrapper(g):     yield from g 

That works, and we eliminated one line of code. And probably the intent is a little bit clearer (or not). But nothing life changing.

Sending data to a generator (coroutine) using yield from - Part 1

Now let's do something more interesting. Let's create a coroutine called writer that accepts data sent to it and writes to a socket, fd, etc.

def writer():     """A coroutine that writes data *sent* to it to fd, socket, etc."""     while True:         w = (yield)         print('>> ', w) 

Now the question is, how should the wrapper function handle sending data to the writer, so that any data that is sent to the wrapper is transparently sent to the writer()?

def writer_wrapper(coro):     # TBD     pass  w = writer() wrap = writer_wrapper(w) wrap.send(None)  # "prime" the coroutine for i in range(4):     wrap.send(i)  # Expected result >>  0 >>  1 >>  2 >>  3 

The wrapper needs to accept the data that is sent to it (obviously) and should also handle the StopIteration when the for loop is exhausted. Evidently just doing for x in coro: yield x won't do. Here is a version that works.

def writer_wrapper(coro):     coro.send(None)  # prime the coro     while True:         try:             x = (yield)  # Capture the value that's sent             coro.send(x)  # and pass it to the writer         except StopIteration:             pass 

Or, we could do this.

def writer_wrapper(coro):     yield from coro 

That saves 6 lines of code, make it much much more readable and it just works. Magic!

Sending data to a generator yield from - Part 2 - Exception handling

Let's make it more complicated. What if our writer needs to handle exceptions? Let's say the writer handles a SpamException and it prints *** if it encounters one.

class SpamException(Exception):     pass  def writer():     while True:         try:             w = (yield)         except SpamException:             print('***')         else:             print('>> ', w) 

What if we don't change writer_wrapper? Does it work? Let's try

# writer_wrapper same as above  w = writer() wrap = writer_wrapper(w) wrap.send(None)  # "prime" the coroutine for i in [0, 1, 2, 'spam', 4]:     if i == 'spam':         wrap.throw(SpamException)     else:         wrap.send(i)  # Expected Result >>  0 >>  1 >>  2 *** >>  4  # Actual Result >>  0 >>  1 >>  2 Traceback (most recent call last):   ... redacted ...   File ... in writer_wrapper     x = (yield) __main__.SpamException 

Um, it's not working because x = (yield) just raises the exception and everything comes to a crashing halt. Let's make it work, but manually handling exceptions and sending them or throwing them into the sub-generator (writer)

def writer_wrapper(coro):     """Works. Manually catches exceptions and throws them"""     coro.send(None)  # prime the coro     while True:         try:             try:                 x = (yield)             except Exception as e:   # This catches the SpamException                 coro.throw(e)             else:                 coro.send(x)         except StopIteration:             pass 

This works.

# Result >>  0 >>  1 >>  2 *** >>  4 

But so does this!

def writer_wrapper(coro):     yield from coro 

The yield from transparently handles sending the values or throwing values into the sub-generator.

This still does not cover all the corner cases though. What happens if the outer generator is closed? What about the case when the sub-generator returns a value (yes, in Python 3.3+, generators can return values), how should the return value be propagated? That yield from transparently handles all the corner cases is really impressive. yield from just magically works and handles all those cases.

I personally feel yield from is a poor keyword choice because it does not make the two-way nature apparent. There were other keywords proposed (like delegate but were rejected because adding a new keyword to the language is much more difficult than combining existing ones.

In summary, it's best to think of yield from as a transparent two way channel between the caller and the sub-generator.

References:

  1. PEP 380 - Syntax for delegating to a sub-generator (Ewing) [v3.3, 2009-02-13]
  2. PEP 342 - Coroutines via Enhanced Generators (GvR, Eby) [v2.5, 2005-05-10]
like image 86
Praveen Gollakota Avatar answered Nov 04 '22 13:11

Praveen Gollakota


What are the situations where "yield from" is useful?

Every situation where you have a loop like this:

for x in subgenerator:   yield x 

As the PEP describes, this is a rather naive attempt at using the subgenerator, it's missing several aspects, especially the proper handling of the .throw()/.send()/.close() mechanisms introduced by PEP 342. To do this properly, rather complicated code is necessary.

What is the classic use case?

Consider that you want to extract information from a recursive data structure. Let's say we want to get all leaf nodes in a tree:

def traverse_tree(node):   if not node.children:     yield node   for child in node.children:     yield from traverse_tree(child) 

Even more important is the fact that until the yield from, there was no simple method of refactoring the generator code. Suppose you have a (senseless) generator like this:

def get_list_values(lst):   for item in lst:     yield int(item)   for item in lst:     yield str(item)   for item in lst:     yield float(item) 

Now you decide to factor out these loops into separate generators. Without yield from, this is ugly, up to the point where you will think twice whether you actually want to do it. With yield from, it's actually nice to look at:

def get_list_values(lst):   for sub in [get_list_values_as_int,                get_list_values_as_str,                get_list_values_as_float]:     yield from sub(lst) 

Why is it compared to micro-threads?

I think what this section in the PEP is talking about is that every generator does have its own isolated execution context. Together with the fact that execution is switched between the generator-iterator and the caller using yield and __next__(), respectively, this is similar to threads, where the operating system switches the executing thread from time to time, along with the execution context (stack, registers, ...).

The effect of this is also comparable: Both the generator-iterator and the caller progress in their execution state at the same time, their executions are interleaved. For example, if the generator does some kind of computation and the caller prints out the results, you'll see the results as soon as they're available. This is a form of concurrency.

That analogy isn't anything specific to yield from, though - it's rather a general property of generators in Python.

like image 22
Niklas B. Avatar answered Nov 04 '22 15:11

Niklas B.