Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python : Behaviour of send() in generators

I was experimenting with generators in python 3 and wrote this rather contrived generator :

def send_gen():
    print("    send_gen(): will yield 1")
    x = yield 1
    print("    send_gen(): sent in '{}'".format(x))
    # yield  # causes StopIteration when left out


gen = send_gen()
print("yielded {}".format(gen.__next__()))

print("running gen.send()")
gen.send("a string")

Output:

    send_gen(): will yield 1
yielded 1
running gen.send()
    send_gen(): sent in 'a string'
Traceback (most recent call last):
  File "gen_test.py", line 12, in <module>
    gen.send("a string")
StopIteration

So gen.__next__() reaches the line x = yield 1 and yields 1. I thought x would be assigned to None, then gen.send() would look for the next yield statement because x = yield 1 is "used", then get a StopIteration.

Instead, what seems to have happened is that x gets sent "a string", which is printed, then then python attempts to look for the next yield and gets a StopIteration.

So i try this:

def send_gen():
    x = yield 1
    print("    send_gen(): sent in '{}'".format(x))


gen = send_gen()
print("yielded : {}".format(gen.send(None)))

Output :

yielded : 1

But now there's no error. send() doesn't appear to have tried to look for the next yield statement after assigning x to None.

Why is the behaviour slightly different ? Does this have to do with how I started the generators ?

like image 230
peonicles Avatar asked May 03 '16 06:05

peonicles


People also ask

What does send () do in Python?

The send() method returns the next value yielded by the generator, or raises StopIteration if the generator exits without yielding another value. When send() is called to start the generator, it must be called with None as the argument, because there is no yield expression that could receive the value.

What are generator expressions in Python?

A generator expression is an expression that returns a generator object. Basically, a generator function is a function that contains a yield statement and returns a generator object. The squares generator function returns a generator object that produces square numbers of integers from 0 to length - 1 .

How does Python know if a function is a generator?

Create Generators in Python If a function contains at least one yield statement (it may contain other yield or return statements), it becomes a generator function. Both yield and return will return some value from a function.

Are generators lazy in Python?

Generators are memory efficient since they only require memory for the one value they yield. Generators are lazy: they only yield values when explicitly asked.


1 Answers

The behaviour is not different; you never advanced beyond the first yield expression in the generator in the second setup. Note that StopIteration is not an error; it is normal behaviour, the expected signal to be fired whenever a generator has ended. In your second example, you just never reached the end of the generator.

Whenever a generator reaches a yield expression, execution pauses right there, the expression can't produce anything inside the generator until it is resumed. Either gen.__next__() or a gen.send() will both resume execution from that point, with the yield expression either producing the value passed in by gen.send(), or None. You could see gen.__next__() as a gen.send(None) if that helps. The one thing to realise here is that gen.send() has yield return the sent value first, and then the generator continues on to the next yield.

So, given your first example generator, this happens:

  1. gen = send_gen() creates the generator object. The code is paused at the very top of the function, nothing is executed.

  2. You either call gen.__next__() or gen.send(None); the generator commences and executes until the first yield expression:

    print("    send_gen(): will yield 1")
    yield 1
    

    and execution now pauses. The gen.__next__() or gen.send(None) calls now return 1, the value yielded by yield 1. Because the generator is now paused, the x = ... assignment can't yet take place! That'll only happen when the generator is resumed again.

  3. You call gen.send("a string") in your first example, don't make any call in the second. So for the first example, the generator function is resumed now:

    x = <return value of the yield expression>  # 'a string' in this case
    print("    send_gen(): sent in '{}'".format(x))
    

    and now the function ends, so StopIteration is raised.

Because you never resumed the generator in your second example, the end of the generator is not reached and no StopIteration exception is raised.

Note that because a generator starts at the top of a function, there is no yield expression at that point to return whatever you sent with gen.send() so a first gen.send() value must always be None or an exception is raised. It is best to use an explicit gen.__next__() (or, rather a next(gen) function call) to 'prime' the generator so it'll be paused at the first yield expression.

like image 53
Martijn Pieters Avatar answered Nov 08 '22 08:11

Martijn Pieters