Jerzy Karczmarczuk
Jerzy Karczmarczuk

Reputation: 101

What happens if you send() to a generator *expression* in Python?

I was surprised to find that, say, ge=(x*x for x in [1,2,3]) accepts the .send method. The argument of the first call must be None, as with any other generator , but the behaviour of further calls, say, ans=ge.send(99) seems identical to ans=next(ge).

Where goes my 99? There are no yield expressions within ge, nothing to be assigned. Is the value injected simply discarded (as I suspect), or there is some Mystery involved?

Has anybody seen that?

Upvotes: 2

Views: 161

Answers (2)

Jerzy Karczmarczuk
Jerzy Karczmarczuk

Reputation: 101

Thank you, all. So yes, the arg of send is discarded, but the fact that send is accepted seems to be an anomaly.

Another, related bug has been already commented here (page 32139885), the yield expression should be forbidden in genexps, but it isn't. The form ge=((yield x*x) for x in [1,2,3]) is accepted, and .send() works.

The answer returned by send is then a mixture of elements in the internal iterable, and the args of send... If I am not mistaken, GvR wrote that in Python 3.8 this (the yield expression) will be treated as an error, and in 3.7 it should signal that it is deprecated. (People agreed that it was confusing.)

But I tested that in Python 3.7 (Anaconda, Windows 64), and I got no deprecation warning. Anyway, this seems to be a real bug, not a feature to be deprecated. I believe that for the moment there is nothing more to say...

JK

Upvotes: 0

user2357112
user2357112

Reputation: 281604

Same thing as if you send to the equivalent generator created with a generator function:

def genfunc(outer_iterable):
    for x in outer_iterable:
        yield x*x

ge = genfunc([1, 2, 3])

which is to say, the send argument gets discarded.

We can disassemble the bytecode for further confirmation:

import dis

ge=(x*x for x in [1,2,3])

print('Genexp:')
dis.dis(ge)

def genfunc(outer_iterable):
    for x in outer_iterable:
        yield x*x

ge = genfunc([1, 2, 3])

print()
print('Generator function:')
dis.dis(ge)

Output:

Genexp:
  3           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                15 (to 21)
              6 STORE_FAST               1 (x)
              9 LOAD_FAST                1 (x)
             12 LOAD_FAST                1 (x)
             15 BINARY_MULTIPLY
             16 YIELD_VALUE
             17 POP_TOP
             18 JUMP_ABSOLUTE            3
        >>   21 LOAD_CONST               0 (None)
             24 RETURN_VALUE

Generator function:
  9           0 SETUP_LOOP              23 (to 26)
              3 LOAD_FAST                0 (outer_iterable)
              6 GET_ITER
        >>    7 FOR_ITER                15 (to 25)
             10 STORE_FAST               1 (x)

 10          13 LOAD_FAST                1 (x)
             16 LOAD_FAST                1 (x)
             19 BINARY_MULTIPLY
             20 YIELD_VALUE
             21 POP_TOP
             22 JUMP_ABSOLUTE            7
        >>   25 POP_BLOCK
        >>   26 LOAD_CONST               0 (None)
             29 RETURN_VALUE

The genexp and the generator created through the generator function have very similar disassemblies, and in both, the YIELD_VALUE is immediately followed by a POP_TOP that discards any value sent in from send.

Upvotes: 3

Related Questions