3

There's a number of good questions about similar matters, e.g.

python generator "send" function purpose?

What does the "yield" keyword do?

Lets get back to a definition of "send":

Resumes the execution and “sends” a value into the generator function. The value argument becomes the result of the current yield expression. 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

But I feel I am missing something important. Here's my example with 3 send calls, including the initial one with a None value just to initialize a generator:

def multiplier():
    while True:
        m = yield               # Line #3
        print('m = ' + str(m))  # Line #4
        yield str(m * 2)        # Line #5
        yield str(m * 3)        # Line #6

#------------------------

it = multiplier()

print('it.send(None): ')
print(str(it.send(None)))
print('--------------')

print('it.send(10): ')
print(it.send(10))
print('--------------')

print('it.send(100): ')
print(it.send(100))
print('--------------')

And here's an output:

it.send(None): 
None
--------------
it.send(10): 
m = 10
20
--------------
it.send(100): 
30
--------------

Questions:

  • What happens exactly when I use it.send(10) in a line #5. If we follow the definition, the generator execution resumes. Generator accepts 10 as input value and uses it in a current yield expression. It is yield str(m * 2) in my example, but then how m is set to 10. When did that happen. Is that because of the reference between m and yield in a line #3?

  • What happens in a line #6 it.send(10) and why output is still 30? Does it mean that the reference in my previous question only worked once?

Note: If I've changed my example and added a line m = yield between lines #5 and #6 and then use next(it) after print(it.send(10)) - in that case the output starts to make sense: 20 and 300

Axalix
  • 2,831
  • 1
  • 20
  • 37
  • 1
    There's only one `yield` expression in your code: `m = yield`. Even if you send a value, `m` will not be reassigned until the next iteration of the `while` loop. – vaultah Dec 17 '17 at 00:06
  • @vaultah When is it set? After a first or second `send` call? Is the first `send` just `yield`-s and a second one changes / set `yield` value and assigns it to `m` and then stops after a second "yield" call? – Axalix Dec 17 '17 at 00:10
  • 1
    @Axalix Before the first `send` nothing in the generator was executed. When calling first `send`, generator is executed until the `yield` in `m = yield` is entered. On second `send`, `yield` returns with the parameter of `send` and execution is continued until the `yield str(m * 2)` is entered. – Michael Butscher Dec 17 '17 at 00:19
  • @MichaelButscher that makes sense for me, but in which moment does Python assign a value to `m`? Does that happen on a second `send` call? – Axalix Dec 17 '17 at 00:29
  • 1
    @Axalix Yes. Second `send` lets the `yield` expression return with the parameter value and the value is then assigned to `m` – Michael Butscher Dec 17 '17 at 00:35
  • I see. So priority of `yield` is higher than `=`. And execution doesn't actually happen from left to right, but stops on `yield` and continues with `=` during next iteration. – Axalix Dec 17 '17 at 00:45

1 Answers1

2

Your generator function has three yield expressions, but you're throwing away the value from two of them (lines 5 and 6). If you did something with the values there, you'd see the 100 being used in the function. If you kept running your example, the fifth time you called send would cause the generator to update m to a new value.

Lets walk through the code that does the send calls in your example, and see what the generator is doing at the same time:

it = multiplier()

At this point the generator object has been created and saved to it. The generator code has not started running yet, it's paused at the start of the function's code.

print(str(it.send(None)))

This starts running the generator function's code. The value sent must be None or you'll get an error. The function never sees that value. It's more common to use next to start up a generator, since next(it) is equivalent to it.send(None).

The generator function runs until line 3, where the first yield appears. Since you're not yielding any particular value, the return value from send is None (which gets printed).

print(it.send(10))

This value gets sent to the generator and becomes the value of the yield expression on line 3. So 10 gets stored as m, and the code prints it out on line 4. The generator function keeps running to line 5, where it reaches the next yield expression. Since it's yielding str(m * 2), the calling code gets "20" and prints that.

print(it.send(100))

The 100 value gets sent into the generator as the value of the yield on line 4. That value is ignored, since you're not using the yield as an expression but as a statement. Just like putting 100 on a line by itself, this is perfectly legal, but maybe not very useful. The code goes on to line 5 where it yields str(m * 3), or "30", which gets printed by the calling code.

That's where your driving code stops, but the generator is still alive, and you could send more values to it (and get more values back). The next value you send to the generator would also be ignored, just like the 100 was, but the value after that would end up as a new m value when the while loop in the generator returned to the top and the line 3 yield was reached.

I suspect that some of your confusion with send in this code has to do with the fact that you're using yield both as an expression and as a statement. Probably you don't want to be doing both. Usually you'll either care about all the values being sent into the generator, or you don't care about any of them. If you want to yield several values together (like n*2 and n*3), you could yield a tuple rather than a single item.

Here's a modified version of your code that I think might be easier for you to play with and understand:

def multiplier():
    print("top of generator")
    m = yield # nothing to yield the first time, just a value we get
    print("before loop, m =", m)
    while True:
        print("top of loop, m =", m)
        m = yield m * 2, m * 3         # we always care about the value we're sent
        print("bottom of loop, m =", m)

print("calling generator")
it = multiplier()

print("calling next")
next(it)   # this is equivalent to it.send(None)

print("sending 10")
print(it.send(10))

print("sending 20")
print(it.send(20))

print("sending 100")
print(it.send(100))
Blckknght
  • 100,903
  • 11
  • 120
  • 169