248

I understand yield. But what does a generator's send function do? The documentation says:

generator.send(value)

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.

What does that mean? I thought value was the input to the generator function? The phrase "The send() method returns the next value yielded by the generator" seems to be also the exact purpose of yield, which also returns the next value yielded by the generator.

Is there an example of a generator utilizing send that accomplishes something yield cannot?

Mateen Ulhaq
  • 24,552
  • 19
  • 101
  • 135
Tommy
  • 12,588
  • 14
  • 59
  • 110
  • 6
    duplicate:http://stackoverflow.com/questions/12637768/python-3-send-method-of-generators – Bas Swinckels Oct 10 '13 at 17:49
  • 3
    Added another real life example (reading from FTP) when [callbacks are turned into generator used from inside](http://stackoverflow.com/a/36946209/346478) – Jan Vlcinsky Apr 29 '16 at 19:46
  • 4
    It's worth mentioning that "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.", quoted from the official doc and for which the citation in the question is missing. – Rick Feb 10 '20 at 04:28

9 Answers9

237

It's used to send values into a generator that just yielded. Here is an artificial (non-useful) explanatory example:

>>> def double_inputs():
...     while True:
...         x = yield
...         yield x * 2
...
>>> gen = double_inputs()
>>> next(gen)       # run up to the first yield
>>> gen.send(10)    # goes into 'x' variable
20
>>> next(gen)       # run up to the next yield
>>> gen.send(6)     # goes into 'x' again
12
>>> next(gen)       # run up to the next yield
>>> gen.send(94.3)  # goes into 'x' again
188.5999999999999

You can't do this just with yield.

As to why it's useful, one of the best use cases I've seen is Twisted's @defer.inlineCallbacks. Essentially it allows you to write a function like this:

@defer.inlineCallbacks
def doStuff():
    result = yield takesTwoSeconds()
    nextResult = yield takesTenSeconds(result * 10)
    defer.returnValue(nextResult / 10)

What happens is that takesTwoSeconds() returns a Deferred, which is a value promising a value will be computed later. Twisted can run the computation in another thread. When the computation is done, it passes it into the deferred, and the value then gets sent back to the doStuff() function. Thus the doStuff() can end up looking more or less like a normal procedural function, except it can be doing all sorts of computations & callbacks etc. The alternative before this functionality would be to do something like:

def doStuff():
    returnDeferred = defer.Deferred()
    def gotNextResult(nextResult):
        returnDeferred.callback(nextResult / 10)
    def gotResult(result):
        takesTenSeconds(result * 10).addCallback(gotNextResult)
    takesTwoSeconds().addCallback(gotResult)
    return returnDeferred

It's a lot more convoluted and unwieldy.

Evert Heylen
  • 960
  • 9
  • 28
Claudiu
  • 224,032
  • 165
  • 485
  • 680
  • 3
    Can you explain what the purpose of this is? Why can this not be re-created with double_inputs(startingnumber) and yield? – Tommy Oct 10 '13 at 17:49
  • @Tommy: oh because the values you got has nothing to do with the previous one. let me change the example – Claudiu Oct 10 '13 at 17:51
  • why would you use this then over a simple function that doubles its input?? – Tommy Oct 10 '13 at 17:53
  • 6
    @Tommy: You wouldn't. The first example is just to explain what it does. The second example is for an actually useful use case. – Claudiu Oct 10 '13 at 17:59
  • @Tommy: hmm not quite. not if the generator has internal state. well you can always mimic it using other stuff - but it makes some things a lot easier. really the only good example i have is the twisted one. even that one you can have the same functionality, it's just a lot messier – Claudiu Oct 10 '13 at 18:37
  • 3
    @Tommy: I would say if you really wanna know check out [this presentation](http://www.dabeaz.com/coroutines/index.html) and work through it all. A short answer won't suffice because then you'll just say "But can't I just do it like this?" etc. – Claudiu Oct 10 '13 at 18:45
  • 1
    Very nice solution, just one mention next() has changed in python 3.x. use gen.__next__() now. – Hades Jul 25 '20 at 02:10
  • That's a nice explanation. – Antonio Sesto Dec 04 '20 at 06:35
  • So `send` makes `yield` evaluate to something inside the generator! – JamesTheAwesomeDude May 05 '21 at 22:45
  • Would modern python just use the async keyword? – Tom McLean Sep 01 '22 at 08:32
144

This function is to write coroutines

def coroutine():
    for i in range(1, 10):
        print("From generator {}".format((yield i)))
c = coroutine()
c.send(None)
try:
    while True:
        print("From user {}".format(c.send(1)))
except StopIteration: pass

prints

From generator 1
From user 2
From generator 1
From user 3
From generator 1
From user 4
...

See how the control is being passed back and forth? Those are coroutines. They can be used for all kinds of cool things like asynch IO and similar.

Think of it like this, with a generator and no send, it's a one way street

==========       yield      ========
Generator |   ------------> | User |
==========                  ========

But with send, it becomes a two way street

==========       yield       ========
Generator |   ------------>  | User |
==========    <------------  ========
                  send

Which opens up the door to the user customizing the generators behavior on the fly and the generator responding to the user.

vsminkov
  • 10,912
  • 2
  • 38
  • 50
daniel gratzer
  • 52,833
  • 11
  • 94
  • 134
  • 6
    but a generator function can take parameters. How does "Send" go beyond sending a parameter to the generator? – Tommy Oct 10 '13 at 17:57
  • 23
    @Tommy Because you can't change the parameters to a generator as it runs. You give it parameters, it runs, done. With send, you give it parameters, it runs for a bit, you send it a value and it does something different, repeat – daniel gratzer Oct 10 '13 at 18:00
  • Why is this different than just sending a new parameter (by re-calling the generator) when you decide to change the parameter? – Tommy Oct 10 '13 at 18:04
  • 3
    @Tommy This will restart the generator, which will cause you to redo lots of work – daniel gratzer Oct 10 '13 at 18:06
  • ah, now that makes sense. Thats what I was looking for. So technically you *can* implement an identical generator using only yield and a parameter, but it would be less efficient? – Tommy Oct 10 '13 at 18:07
  • Could you please provide a python3 version of your code? – qed Dec 11 '14 at 16:10
  • 1
    You provided wrong printout example. First element yielded from generator will be in place of `c.send(None)` and it lost. Printout should look like: `From generator 1 From user 2 From generator 2 .... ` – Felix Markov May 19 '15 at 09:03
  • Actually I get: From Generator 1 From user 2 From Generator 1 From user 3 From Generator 1 .... – mangolier Dec 05 '15 at 10:32
  • 11
    Could you please explain the purpose of sending a None before everything? – Shubham Aggarwal Jan 19 '16 at 09:48
  • 5
    @ShubhamAggarwal It is done to 'start' the generator. It is just something that needs to be done. It makes some sense when you think about it since the first time you call `send()` the generator has not reached the keyword `yield` yet. – Michael Jan 28 '17 at 03:07
  • 1
    @Michale so confusing. So when I execute send(), what lines execute exactly? – lucid_dreamer May 30 '18 at 18:07
  • 2
    `yield i` is wrapped in two consecutive layers of parentheses. If you remove one layer, it's a syntax error. Why? What do those inner parentheses around `yield i` mean? – BallpointBen Feb 03 '20 at 05:11
  • @Shubham, your question *“Could you please explain the purpose of sending a None before everything?”* I explained in detail [here](https://stackoverflow.com/a/65616291/7023590). – MarianD Jan 07 '21 at 16:41
97

This may help someone. Here is a generator that is unaffected by send function. It takes in the number parameter on instantiation and is unaffected by send:

>>> def double_number(number):
...     while True:
...         number *=2 
...         yield number
... 
>>> c = double_number(4)
>>> c.send(None)
8
>>> c.next()
16
>>> c.next()
32
>>> c.send(8)
64
>>> c.send(8)
128
>>> c.send(8)
256

Now here is how you would do the same type of function using send, so on each iteration you can change the value of number:

def double_number(number):
    while True:
        number *= 2
        number = yield number

Here is what that looks like, as you can see sending a new value for number changes the outcome:

>>> def double_number(number):
...     while True:
...         number *= 2
...         number = yield number
...
>>> c = double_number(4)
>>> 
>>> c.send(None)
8
>>> c.send(5) #10
10
>>> c.send(1500) #3000
3000
>>> c.send(3) #6
6

You can also put this in a for loop as such:

for x in range(10):
    n = c.send(n)
    print n

For more help check out this great tutorial.

Jonas G. Drange
  • 8,749
  • 2
  • 27
  • 38
radtek
  • 34,210
  • 11
  • 144
  • 111
44

The send() method controls what the value to the left of the yield expression will be.

To understand how yield differs and what value it holds, lets first quickly refresh on the order python code is evaluated.

Section 6.15 Evaluation order

Python evaluates expressions from left to right. Notice that while evaluating an assignment, the right-hand side is evaluated before the left-hand side.

So an expression a = b the right hand side is evaluated first.

As the following demonstrates that a[p('left')] = p('right') the right hand side is evaluated first.

>>> def p(side):
...     print(side)
...     return 0
... 
>>> a[p('left')] = p('right')
right
left
>>> 
>>> 
>>> [p('left'), p('right')]
left
right
[0, 0]

What does yield do?, yield, suspends execution of the function and returns to the caller, and resumes execution at the same place it left off prior to suspending.

Where exactly is execution suspended? You might have guessed it already... the execution is suspended between the right and left side of the yield expression. So new_val = yield old_val the execution is halted at the = sign, and the value on the right (which is before suspending, and is also the value returned to the caller) may be something different then the value on the left (which is the value being assigned after resuming execution).

yield yields 2 values, one to the right and another to the left.

How do you control the value to the left hand side of the yield expression? via the .send() method.

6.2.9. Yield expressions

The value of the yield expression after resuming depends on the method which resumed the execution. If __next__() is used (typically via either a for or the next() builtin) then the result is None. Otherwise, if send() is used, then the result will be the value passed in to that method.

user2059857
  • 1,191
  • 13
  • 15
  • 2
    Your explanation helped me understand how coroutines work so much better than other examples above!! Thank you :) – rtindru Apr 27 '22 at 19:47
  • Would it not be more correct to say that `send()` controls what the result of the *yield expression* will be? – bool3max May 01 '23 at 12:33
  • Which result? The one returned to the caller or the one assigned to the variable inside the function. So while that is true, it is still not clear enough. – user2059857 May 02 '23 at 14:44
26

Some use cases for using generator and send()

Generators with send() allow:

  • remembering internal state of the execution
    • what step we are at
    • what is current status of our data
  • returning sequence of values
  • receiving sequence of inputs

Here are some use cases:

Watched attempt to follow a recipe

Let us have a recipe, which expects predefined set of inputs in some order.

We may:

  • create a watched_attempt instance from the recipe
  • let it get some inputs
  • with each input return information about what is currently in the pot
  • with each input check, that the input is the expected one (and fail if it is not)

    def recipe():
        pot = []
        action = yield pot
        assert action == ("add", "water")
        pot.append(action[1])
    
        action = yield pot
        assert action == ("add", "salt")
        pot.append(action[1])
    
        action = yield pot
        assert action == ("boil", "water")
    
        action = yield pot
        assert action == ("add", "pasta")
        pot.append(action[1])
    
        action = yield pot
        assert action == ("decant", "water")
        pot.remove("water")
    
        action = yield pot
        assert action == ("serve")
        pot = []
        yield pot
    

To use it, first create the watched_attempt instance:

>>> watched_attempt = recipe()                                                                         
>>> watched_attempt.next()                                                                                     
[]                                                                                                     

The call to .next() is necessary to start execution of the generator.

Returned value shows, our pot is currently empty.

Now do few actions following what the recipe expects:

>>> watched_attempt.send(("add", "water"))                                                                     
['water']                                                                                              
>>> watched_attempt.send(("add", "salt"))                                                                      
['water', 'salt']                                                                                      
>>> watched_attempt.send(("boil", "water"))                                                                    
['water', 'salt']                                                                                      
>>> watched_attempt.send(("add", "pasta"))                                                                     
['water', 'salt', 'pasta']                                                                             
>>> watched_attempt.send(("decant", "water"))                                                                  
['salt', 'pasta']                                                                                      
>>> watched_attempt.send(("serve"))                                                                            
[] 

As we see, the pot is finally empty.

In case, one would not follow the recipe, it would fail (what could be desired outcome of watched attempt to cook something - just learning we did not pay enough attention when given instructions.

>>> watched_attempt = running.recipe()                                                                         
>>> watched_attempt.next()                                                                                     
[]                                                                                                     
>>> watched_attempt.send(("add", "water"))                                                                     
['water']                                                                                              
>>> watched_attempt.send(("add", "pasta")) 

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-21-facdf014fe8e> in <module>()
----> 1 watched_attempt.send(("add", "pasta"))

/home/javl/sandbox/stack/send/running.py in recipe()
     29
     30     action = yield pot
---> 31     assert action == ("add", "salt")
     32     pot.append(action[1])
     33

AssertionError:

Notice, that:

  • there is linear sequence of expected steps
  • the steps may differ (some are removing, some are adding to the pot)
  • we manage to do all that by a functions/generator - no need to use complex class or similar strutures.

Running totals

We may use the generator to keep track of running total of values sent to it.

Any time we add a number, count of inputs and total sum is returned (valid for the moment previous input was send into it).

from collections import namedtuple

RunningTotal = namedtuple("RunningTotal", ["n", "total"])


def runningtotals(n=0, total=0):
    while True:
        delta = yield RunningTotal(n, total)
        if delta:
            n += 1
            total += delta


if __name__ == "__main__":
    nums = [9, 8, None, 3, 4, 2, 1]

    bookeeper = runningtotals()
    print bookeeper.next()
    for num in nums:
        print num, bookeeper.send(num)

The output would look like:

RunningTotal(n=0, total=0)
9 RunningTotal(n=1, total=9)
8 RunningTotal(n=2, total=17)
None RunningTotal(n=2, total=17)
3 RunningTotal(n=3, total=20)
4 RunningTotal(n=4, total=24)
2 RunningTotal(n=5, total=26)
1 RunningTotal(n=6, total=27)
Asish M.
  • 2,588
  • 1
  • 16
  • 31
Jan Vlcinsky
  • 42,725
  • 12
  • 101
  • 98
  • 5
    I run your example and in python 3 seems that the watched_attempt.next() has to be replaced by next(watched_attempt). – thanos.a Dec 26 '19 at 15:02
17

The send method implements coroutines.

If you haven't encountered Coroutines they are tricky to wrap your head around because they change the way a program flows. You can read a good tutorial for more details.

Jochen Ritzel
  • 104,512
  • 31
  • 200
  • 194
11

The word "yield" has two meanings: to produce something (e.g., to yield corn), and to halt to let someone/thing else continue (e.g., cars yielding to pedestrians). Both definitions apply to Python's yield keyword; what makes generator functions special is that unlike in regular functions, values can be "returned" to the caller while merely pausing, not terminating, a generator function.

It is easiest to imagine a generator as one end of a bidirectional pipe with a "left" end and a "right" end; this pipe is the medium over which values are sent between the generator itself and the generator function's body. Each end of the pipe has two operations: push, which sends a value and blocks until the other end of the pipe pulls the value, and returns nothing; and pull, which blocks until the other end of the pipe pushes a value, and returns the pushed value. At runtime, execution bounces back and forth between the contexts on either side of the pipe -- each side runs until it sends a value to the other side, at which point it halts, lets the other side run, and waits for a value in return, at which point the other side halts and it resumes. In other words, each end of the pipe runs from the moment it receives a value to the moment it sends a value.

The pipe is functionally symmetric, but -- by convention I'm defining in this answer -- the left end is only available inside the generator function's body and is accessible via the yield keyword, while the right end is the generator and is accessible via the generator's send function. As singular interfaces to their respective ends of the pipe, yield and send do double duty: they each both push and pull values to/from their ends of the pipe, yield pushing rightward and pulling leftward while send does the opposite. This double duty is the crux of the confusion surrounding the semantics of statements like x = yield y. Breaking yield and send down into two explicit push/pull steps will make their semantics much more clear:

  1. Suppose g is the generator. g.send pushes a value leftward through the right end of the pipe.
  2. Execution within the context of g pauses, allowing the generator function's body to run.
  3. The value pushed by g.send is pulled leftward by yield and received on the left end of the pipe. In x = yield y, x is assigned to the pulled value.
  4. Execution continues within the generator function's body until the next line containing yield is reached.
  5. yield pushes a value rightward through the left end of the pipe, back up to g.send. In x = yield y, y is pushed rightward through the pipe.
  6. Execution within the generator function's body pauses, allowing the outer scope to continue where it left off.
  7. g.send resumes and pulls the value and returns it to the user.
  8. When g.send is next called, go back to Step 1.

While cyclical, this procedure does have a beginning: when g.send(None) -- which is what next(g) is short for -- is first called (it is illegal to pass something other than None to the first send call). And it may have an end: when there are no more yield statements to be reached in the generator function's body.

Do you see what makes the yield statement (or more accurately, generators) so special? Unlike the measly return keyword, yield is able to pass values to its caller and receive values from its caller all without terminating the function it lives in! (Of course, if you do wish to terminate a function -- or a generator -- it's handy to have the return keyword as well.) When a yield statement is encountered, the generator function merely pauses, and then picks back up right where it left off upon being sent another value. And send is just the interface for communicating with the inside of a generator function from outside it.

If we really want to break this push/pull/pipe analogy down as far as we can, we end up with the following pseudocode that really drives home that, aside from steps 1-5, yield and send are two sides of the same coin pipe:

  1. right_end.push(None) # the first half of g.send; sending None is what starts a generator
  2. right_end.pause()
  3. left_end.start()
  4. initial_value = left_end.pull()
  5. if initial_value is not None: raise TypeError("can't send non-None value to a just-started generator")
  6. left_end.do_stuff()
  7. left_end.push(y) # the first half of yield
  8. left_end.pause()
  9. right_end.resume()
  10. value1 = right_end.pull() # the second half of g.send
  11. right_end.do_stuff()
  12. right_end.push(value2) # the first half of g.send (again, but with a different value)
  13. right_end.pause()
  14. left_end.resume()
  15. x = left_end.pull() # the second half of yield
  16. goto 6

The key transformation is that we have split x = yield y and value1 = g.send(value2) each into two statements: left_end.push(y) and x = left_end.pull(); and value1 = right_end.pull() and right_end.push(value2). There are two special cases of the yield keyword: x = yield and yield y. These are syntactic sugar, respectively, for x = yield None and _ = yield y # discarding value.

For specific details regarding the precise order in which values are sent through the pipe, see below.


What follows is a rather long concrete model of the above. First, it should first be noted that for any generator g, next(g) is exactly equivalent to g.send(None). With this in mind we can focus only on how send works and talk only about advancing the generator with send.

Suppose we have

def f(y):  # This is the "generator function" referenced above
    while True:
        x = yield y
        y = x
g = f(1)
g.send(None)  # yields 1
g.send(2)     # yields 2

Now, the definition of f roughly desugars to the following ordinary (non-generator) function:

def f(y):
    bidirectional_pipe = BidirectionalPipe()
    left_end = bidirectional_pipe.left_end
    right_end = bidirectional_pipe.right_end

    def impl():
        initial_value = left_end.pull()
        if initial_value is not None:
            raise TypeError(
                "can't send non-None value to a just-started generator"
            )

        while True:
            left_end.push(y)
            x = left_end.pull()
            y = x

    def send(value):
        right_end.push(value)
        return right_end.pull()

    right_end.send = send

    # This isn't real Python; normally, returning exits the function. But
    # pretend that it's possible to return a value from a function and then
    # continue execution -- this is exactly the problem that generators were
    # designed to solve!
    return right_end
    impl()

The following has happened in this transformation of f:

  1. We've moved the implementation into a nested function.
  2. We've created a bidirectional pipe whose left_end will be accessed by the nested function and whose right_end will be returned and accessed by the outer scope -- right_end is what we know as the generator object.
  3. Within the nested function, the very first thing we do is check that left_end.pull() is None, consuming a pushed value in the process.
  4. Within the nested function, the statement x = yield y has been replaced by two lines: left_end.push(y) and x = left_end.pull().
  5. We've defined the send function for right_end, which is the counterpart to the two lines we replaced the x = yield y statement with in the previous step.

In this fantasy world where functions can continue after returning, g is assigned right_end and then impl() is called. So in our example above, were we to follow execution line by line, what would happen is roughly the following:

left_end = bidirectional_pipe.left_end
right_end = bidirectional_pipe.right_end

y = 1  # from g = f(1)

# None pushed by first half of g.send(None)
right_end.push(None)
# The above push blocks, so the outer scope halts and lets `f` run until
# *it* blocks

# Receive the pushed value, None
initial_value = left_end.pull()

if initial_value is not None:  # ok, `g` sent None
    raise TypeError(
        "can't send non-None value to a just-started generator"
    )

left_end.push(y)
# The above line blocks, so `f` pauses and g.send picks up where it left off

# y, aka 1, is pulled by right_end and returned by `g.send(None)`
right_end.pull()

# Rinse and repeat
# 2 pushed by first half of g.send(2)
right_end.push(2)
# Once again the above blocks, so g.send (the outer scope) halts and `f` resumes

# Receive the pushed value, 2
x = left_end.pull()
y = x  # y == x == 2

left_end.push(y)
# The above line blocks, so `f` pauses and g.send(2) picks up where it left off

# y, aka 2, is pulled by right_end and returned to the outer scope
right_end.pull()

x = left_end.pull()
# blocks until the next call to g.send

This maps exactly to the 16-step pseudocode above.

There are some other details, like how errors are propagated and what happens when you reach the end of the generator (the pipe is closed), but this should make clear how the basic control flow works when send is used.

Using these same desugaring rules, let's look at two special cases:

def f1(x):
    while True:
        x = yield x

def f2():  # No parameter
    while True:
        x = yield x

For the most part they desugar the same way as f, the only differences are how the yield statements are transformed:

def f1(x):
    # ... set up pipe

    def impl():
        # ... check that initial sent value is None

        while True:
            left_end.push(x)
            x = left_end.pull()

    # ... set up right_end


def f2():
    # ... set up pipe

    def impl():
        # ... check that initial sent value is None

        while True:
            left_end.push(x)
            x = left_end.pull()

    # ... set up right_end

In the first, the value passed to f1 is pushed (yielded) initially, and then all values pulled (sent) are pushed (yielded) right back. In the second, x has no value (yet) when it first come times to push, so an UnboundLocalError is raised.

BallpointBen
  • 9,406
  • 1
  • 32
  • 62
  • _"The argument 1 in g = f(1) has been captured normally and assigned to y within f's body, but the while True has not begun yet."_ Why not? Why wouldn't Python try to run this code until it encounters e.g. `yield`? – Josh May 05 '20 at 23:28
  • @Josh The cursor is not advanced until the first call to `send`; it takes one call of `send(None)` to move the cursor to the first `yield` statement, and only then do subsequent `send` calls actually send a "real" value to `yield`. – BallpointBen May 05 '20 at 23:55
  • Thanks - That's interesting, so the interpreter knows that the function `f` **will** `yield` at some point, and thus wait until it gets a `send` from the caller? With a normal function cal the interpreter would just start executing `f` right away, right? After all, there is no AOT compilation of any sort in Python. Are you sure that's the case? (not questioning what you are saying, I'm truly just puzzled by what you wrote here). Where can I read more about how Python knows that it needs to wait before it starts executing the rest of the function? – Josh May 06 '20 at 00:43
  • @Josh I built this mental model just by observing how different toy generators work, without any understanding of Python's internals. However, the fact that the initial `send(None)` yields the appropriate value (e.g., `1`) *without* sending `None` into the generator suggests that the first call to `send` is a special case. It's a tricky interface to design; if you let the first `send` send an arbitrary value, then the order of yielded values and sent values would be off by one compared to what it is currently. – BallpointBen May 06 '20 at 20:59
  • Thanks BallpointBen. Very interesting, I left a question [here](https://stackoverflow.com/questions/61646335/invoking-a-function-that-contains-yield) to see why that's the case. – Josh May 06 '20 at 22:17
5

These confused me too. Here is an example I made when trying to set up a generator which yields and accepts signals in alternating order (yield, accept, yield, accept)...

def echo_sound():

    thing_to_say = '<Sound of wind on cliffs>'
    while True:
        thing_to_say = (yield thing_to_say)
        thing_to_say = '...'.join([thing_to_say]+[thing_to_say[-6:]]*2)
        yield None  # This is the return value of send.

gen = echo_sound()

print 'You are lost in the wilderness, calling for help.'

print '------'
in_message = gen.next()
print 'You hear: "{}"'.format(in_message)
out_message = 'Hello!'
print 'You yell "{}"'.format(out_message)
gen.send(out_message)

print '------'
in_message = gen.next()
print 'You hear: "{}"'.format(in_message)
out_message = 'Is anybody out there?'
print 'You yell "{}"'.format(out_message)
gen.send(out_message)

print '------'
in_message = gen.next()
print 'You hear: "{}"'.format(in_message)
out_message = 'Help!'
print 'You yell "{}"'.format(out_message)
gen.send(out_message)

The output is:

You are lost in the wilderness, calling for help.
------
You hear: "<Sound of wind on cliffs>"
You yell "Hello!"
------
You hear: "Hello!...Hello!...Hello!"
You yell "Is anybody out there?"
------
You hear: "Is anybody out there?...there?...there?"
You yell "Help!"
Peter
  • 12,274
  • 9
  • 71
  • 86
4

itr.send(None) is the same thing as next(itr) and what you're doing is giving the value given by yield in the generator.

Here's an example that clearly shows this, and how it can be used more practically.

def iterator_towards(dest=100):
    value = 0
    while True:
        n = yield value
        if n is not None:
            dest = n
        if dest > value:
            value += 1
        elif dest < value:
            value -= 1
        else:
            return

num = iterator_towards()
for i in num:
    print(i)
    if i == 5:
        num.send(0)

This will print:

0
1
2
3
4
5
3
2
1
0

The code at i == 5 tells it to send 0. This is not None in the iterator_towards and so it changes the value of dest. We then iterate towards 0.

However note, there is no value 4 after the value 5. This is because the nature of .send(0) is that it was yielded the 4 value and that was not printed.

If we add a continue we can re-yield the same value.

def iterator_towards(dest=100):
    value = 0
    while True:
        n = yield value
        if n is not None:
            dest = n
            continue
        if dest > value:
            value += 1
        elif dest < value:
            value -= 1
        else:
            return

Which will allow you to iterate a list but also dynamically send it new destination values onthefly.

Tatarize
  • 10,238
  • 4
  • 58
  • 64
  • 1
    The explanation of what it will print is missing a `0` for the first line. – Sean Mackesey Nov 18 '21 at 20:04
  • "However note, there is no value 4 after the value 5" is wrong - the value 4 will be printed. In the 2nd example the value 5 will be printed twice. So both the examples are wrong in regard to that explanation. – ustulation Mar 14 '23 at 18:10
  • 1
    0 1 2 3 4 5 4 3 2 1 0 Typed it in. Seems to work the way described. – Tatarize Mar 15 '23 at 23:12