Python: I don't understand what's happening with this generator

蹲街弑〆低调 提交于 2019-12-11 03:47:25

问题


I'm curious as to what's happening here. Can someone who knows generators and coroutines well explain this code.

def b():
    for i in range(5):
        yield i
        x = (yield)
        print(x)

def a():
    g = b()
    next(g)
    for i in range(4):
        g.send(5)
        print(next(g))

a()

output

None
1
None
2
None
3
None
4

but when I switch around lines 3 and 4: the lines yield i and x = (yield), I get the following.

5
None
5
None
5
None
5
None

I suspect the problem might me from trying to use the yield statement to both receive and send values in the same function. Is this not possible in Python?

I have successfully written a couple of programs that use coroutines, so I am familiar with the way they work, but I am confused as to the way this snippet of code is behaving. Any insights into this would be appreciated.

Thanks

Edit: Thanks BrenBarn and unutbu for your answers. What's happening here makes more sense when you expand the problem out as such.

def b():
    for i in range(5):
        yield i
        x = yield None

def a():
    g = b()
    print('* got', g.send(None) )
    for i in range(4):
        print('+ got', g.send(5) )
        print('- got', g.send(None))

a()

回答1:


I don't quite get what you're asking, but basically: when you use send, it causes the most-recently-reached yield expression in the generator to evaluate to the value you send. Note also that send advances the generator to the next yield. One thing that may be confusing you is that you are printing the value of x inside the generator, and you are printing the value of next(g) inside b, but the generator is also yielding values at g.send(5), and you aren't printing those.

In your first case, your first send causes the yield i statement to evaluate to 5 inside b, but you don't use this value inside b (you don't assign yield i to anything), so it does nothing. Also, when you do send(5), the generator is yielding None (from the x = (yield) line), but you don't print it so you don't know this. Then you advance the generator again with next(g). The most-recently-reached yield is the x = yield, but next(g) passes no value, so x gets set to None.

In the second case, the parity of the calls is reversed. Now your first send does send to the x = yield line, so x gets set to 5. This send also yields the loop value in b, but you ignore this value in a and don't print it. You then print next(g), which is None. On each subsequent send, b prints the value of x, which is always 5 because that's what you always send, and then a prints the next yielded value, which is always None (because that's what x = yield yields).

I don't quite get what you mean by "using the yield statement to both receive and send values in the same function". You can certainly do this, but you have to realize that: a) a value (None) is still sent even when you call next(g); and b) a value is still yielded when you call g.send(5).




回答2:


Using traceit to step through the program line-by-line:

import sys
import linecache

class SetTrace(object):
    '''
    with SetTrace(monitor):
        ...
    '''
    def __init__(self, func):
        self.func = func
    def __enter__(self):
        sys.settrace(self.func)
        return self
    def passit(self, frame, event, arg):
        return self.passit
    def __exit__(self, ext_type, exc_value, traceback):
        sys.settrace(self.passit)

def traceit(frame, event, arg):
    '''
    http://www.dalkescientific.com/writings/diary/archive/2005/04/20/tracing_python_code.html
    '''
    if event == "line":
        lineno = frame.f_lineno
        filename = frame.f_globals["__file__"]
        if (filename.endswith(".pyc") or
            filename.endswith(".pyo")):
            filename = filename[:-1]
        name = frame.f_globals["__name__"]
        line = linecache.getline(filename, lineno)
        print("%s  # %s:%s" % (line.rstrip(), name, lineno, ))
    return traceit       

def b():
    for i in range(5):
        yield i
        x = (yield)
        print(x)

def a():
    g = b()                    
    next(g)
    for i in range(4):
        g.send(5)
        print(next(g))

with SetTrace(traceit):
    a()        

we obtain

g = b()  # __main__:44
next(g)  # __main__:45                 # runs b until you get to a yield
for i in range(5):  # __main__:38
    yield i  # __main__:39             # stop before the yield; resume a
    ^
for i in range(4):  # __main__:46
    g.send(5)  # __main__:47           # resume b; (yield i) expression evals to 5 then thrown away
    x = (yield)  # __main__:40         # stop before yield; resume a
         ^
    print(next(g))  # __main__:48      # next(g) called; resume b; print not called yet
    print(x)  # __main__:41            # next(g) causes (yield) to evaluate to None
None
for i in range(5):  # __main__:38
    yield i  # __main__:39             # yield 1; resume a; `print(next(g))` prints 1
1
for i in range(4):  # __main__:46
    g.send(5)  # __main__:47           # resume b; (yield i) expression evals to 5 then thrown away

The comments on the right-hand side (above) explain why Python prints None then 1. If you get that far, I think it is clear why you get None, 2, etc. -- it's the same story all over again with different values for i.

The other scenario, where x = (yield) and yield i are reverse can be analyzed similarly.



来源:https://stackoverflow.com/questions/13115135/python-i-dont-understand-whats-happening-with-this-generator

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!