4

I'm playing around with generators and generator expressions and I'm not completely sure that I understand how they work (some reference material):

>>> a = (x for x in range(10))
>>> next(a)
0
>>> next(a)
1
>>> a.send(-1)
2
>>> next(a)
3

So it looks like generator.send was ignored. That makes sense (I guess) because there is no explicit yield expression to catch the sent information ...

However,

>>> a = ((yield x) for x in range(10))
>>> next(a)
0
>>> print next(a)
None
>>> print next(a)
1
>>> print next(a)
None
>>> a.send(-1)  #this send is ignored, Why? ... there's a yield to catch it...
2
>>> print next(a)
None
>>> print next(a)
3
>>> a.send(-1)  #this send isn't ignored
-1

I understand this is pretty far out there, and I (currently) can't think of a use-case for this (so don't ask;)

I'm mostly just exploring to try to figure out how these various generator methods work (and how generator expressions work in general). Why does my second example alternate between yielding a sensible value and None? Also, Can anyone explain why one of my generator.send's was ignored while the other wasn't?

mgilson
  • 300,191
  • 65
  • 633
  • 696

5 Answers5

4

The confusion here is that the generator expression is doing a hidden yield. Here it is in function form:

def foo():
    for x in range(10):
        yield (yield x)

When you do a .send(), what happens is the inner yield x gets executed, which yields x. Then the expression evaluates to the value of the .send, and the next yield yields that. Here it is in clearer form:

def foo():
    for x in range(10):
        sent_value = (yield x)
        yield sent_value

Thus the output is very predictable:

>>> a = foo()
#start it off
>>> a.next() 
0
#execution has now paused at "sent_value = ?"
#now we fill in the "?". whatever we send here will be immediately yielded.
>>> a.send("yieldnow") 
'yieldnow'
#execution is now paused at the 'yield sent_value' expression
#as this is not assigned to anything, whatever is sent now will be lost
>>> a.send("this is lost") 
1
#now we're back where we were at the 'yieldnow' point of the code
>>> a.send("yieldnow") 
'yieldnow'
#etc, the loop continues
>>> a.send("this is lost")
2
>>> a.send("yieldnow")
'yieldnow'
>>> a.send("this is lost")
3
>>> a.send("yieldnow")
'yieldnow'

EDIT: Example usage. By far the coolest one I've seen so far is twisted's inlineCallbacks function. See here for an article explaining it. The nub of it is it lets you yield functions to be run in threads, and once the functions are done, twisted sends the result of the function back into your code. Thus you can write code that heavily relies on threads in a very linear and intuitive manner, instead of having to write tons of little functions all over the place.

See the PEP 342 for more info on the rationale of having .send work with potential use cases (the twisted example I provided is an example of the boon to asynchronous I/O this change offered).

Claudiu
  • 224,032
  • 165
  • 485
  • 680
  • Hmmm ... I suppose you could use this to join 2 lists `['a','c','e']` and `['b','d','f']` into `['a','b','c','d','e','f']` ... – mgilson Sep 07 '12 at 19:47
  • @mgilson: you can also use `+` for that. i'll update my answer soon on a good use case – Claudiu Sep 07 '12 at 19:51
  • How would you use `+` for that? You could do, `c = [None]*6; c[::2] = a; c[1::2] = b`, but I can't see how you would do it with '+'. (I suppose `c = []; for i,j in zip(a,b): c+=[i,j]`, but that's messy at best). – mgilson Sep 07 '12 at 19:54
  • oh oops, i didnt see your output, my bad, i was thinking `['a','c','e']+['b','d','f']` – Claudiu Sep 07 '12 at 19:58
  • That gives you `['a','c','e','b','d','f']`, not `['a','b','c','d','e','f']` ... ;^) – mgilson Sep 07 '12 at 19:59
  • i'd probably prefer `>>> import operator; reduce(operator.add, zip(['a','c','e'], ['b','d','f']))` for that situation. h ow were you thinking of using generators for it? – Claudiu Sep 07 '12 at 20:01
  • The way I was thinking doesn't work out either incidentally ... (after contemplating it a bit more). It also wasn't pretty ... There's also `c = []; map(c.extend,zip(a,b))`, but that's not pretty either. (doesn't your reduce need to be given a start value = `[]` while you're at it?). – mgilson Sep 07 '12 at 20:05
  • @mgilson: aye it would, but i think python special-cases to take the first two items if they are available. – Claudiu Sep 07 '12 at 20:17
3

You're confusing yourself a bit because you actually are generating from two sources: the generator expression (... for x in range(10)) is one generator, but you create another source with the yield. You can see that if do list(a) you'll get [0, None, 1, None, 2, None, 3, None, 4, None, 5, None, 6, None, 7, None, 8, None, 9, None].

Your code is equivalent to this:

>>> def gen():
...     for x in range(10):
...         yield (yield x)

Only the inner yield ("yield x") is "used" in the generator --- it is used as the value of the outer yield. So this generator iterates back and forth between yielding values of the range, and yielding whatever is "sent" to those yields. If you send something to the inner yield, you get it back, but if you happen to send on an even-numbered iteration, the send is sent to the outer yield and is ignored.

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
  • I didn't create 2 generators -- I created 1 generator with 2 yield statements ;-) (apparently) – mgilson Sep 07 '12 at 19:28
  • 2
    You're right, a better way to say it would be that you are generating from two sources. – BrenBarn Sep 07 '12 at 19:29
  • It just blew me away when I realized that `yield` was an expression (not a statement). So I started playing around to try to figure out what I could *actually do with that knowledge*... – mgilson Sep 07 '12 at 19:30
2

This generator translates into:

for i in xrange(10):
    x = (yield i)
    yield x

Result of second call to send()/next() are ignored, because you do nothing with result of one of yields.

Michał Zieliński
  • 1,345
  • 11
  • 13
0

The generator you wrote is equivalent to the more verbose:

def testing():
    for x in range(10):
            x = (yield x)
            yield x

As you can see here, the second yield, which is implicit in the generator expression, does not save the value you pass it, therefore depending on where the generator execution is blocked the send may or may not work.

Bakuriu
  • 98,325
  • 22
  • 197
  • 231
-1

Indeed - the send method is meant to work with a generator object that is the result of a co-routine you have explicitly written. It is difficult to get some meaning to it in a generator expression - though it works.

-- EDIT -- I had previously written this, but it is incorrecct, as yield inside generator expressions are predictable across implementations - though not mentioned in any PEP.

generator expressions are not meant to have the yield keyword - I am not shure the behavior is even defined in this case. We could think a little and get to what is happening on your expression, to meet from where those "None"s are coming from. However, assume that as a side effect of how the yield is implemented in Python (and probably it is even implementation dependent), not as something that should be so.

The correct form for a generator expression, in a simplified manner is:

(<expr> for <variable> in <sequence> [if <expr>])

so, <expr> is evaluated for each value in the <sequence: - not only is yield uneeded, as you should not use it.

Both yield and the send methods are meant to be used in full co-routines, something like:

def doubler():
   value = 0
   while value < 100:
       value = 2 * (yield value)

And you can use it like:

>>> a = doubler()
>>> # Next have to be called once, so the code will run up to the first "yield"
... 
>>> a.next()
0
>>> a.send(10)
20
>>> a.send(20)
40
>>> a.send(23)
46
>>> a.send(51)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> 
jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • 1
    aye. "generator expressions are not meant to have the yield keyword - I am not shure the behavior is even defined in this case." it's perfectly well defined. see [this pastebin](http://pastebin.com/Yq6XeTUg). it's just a bit odd, but it does make sense. – Claudiu Sep 07 '12 at 19:35
  • the None's isn't from an implementation dependent thing, just that he was yielding Nones (calling .next() which sent None into the yield) – Claudiu Sep 07 '12 at 19:38
  • further the python developers actually had to do extra work to make `yield` expression work in a generator, so there's evidence that it was intended to work that way: `>>> [(yield 4) for _ in xrange(5)] ; SyntaxError: 'yield' outside function; >>> ((yield 4) for _ in xrange(5)); at 0x02A75B48>` – Claudiu Sep 07 '12 at 19:39
  • I suppose that I'm still trying to figure out what you can use `send` to actually do. Your example (`doubler`) is much easier to write as a function. I'm not sure what you gain by sending values to a generator instead (other than the headache of starting the thing with `next` and having it raise `StopIteration` on a large enough value -- which you *could do* in a function as well) – mgilson Sep 07 '12 at 19:44
  • @mgilson: the `doubler` is a naive "hello world" example - but the idea is tat you get the remaining of the function state kept, and able to perform operations on the data sent in by `send`. For example, the co-routine could be a method to send its input through a network connection, and be fed one mesage at a time through `send`. – jsbueno Sep 10 '12 at 14:05
  • @Claudiu: The code in your pastebn is a lot easier to understand than the O.P. - however, do you happen have a reference inside a PEP document for that? – jsbueno Sep 10 '12 at 14:05
  • Ok .ntohing in the PEP's, but indeed,a generator expression, once parsed is a "generator object" - just like the object returned by the call of a co-routine (before calling `next`) - therefore a `yield` expression should - and does- work the same way. – jsbueno Sep 10 '12 at 14:19