CtrlPlayer
CtrlPlayer

Reputation: 13

Why yield StopIteration out while loop

I'm using yield from, but I don't know about the influence of while for yield. If I put yield from in a while loop, it works well, but when I cancel the loop at the mean time an exception occurs.

final_result = {}
def sales_sum(pro_name):
    total = 0
    nums = []
    while True:
        x = yield
        print(pro_name+" Sales volume: ", x)
        if not x:
            break
        total += x
        nums.append(x)
    return total, nums

def middle(key):
    while True:
        final_result[key] = yield from sales_sum(key)

def middle2(key):
    final_result[key] = yield from sales_sum(key)

def main(fun):
    data_sets = { "A": [1200, 1500], "B": [28,55,98]}
    for key, data_set in data_sets.items():
        m = fun(key)
        m.send(None)
        for value in data_set:
            m.send(value)
        m.send(None)

if __name__ == '__main__':
    main(middle) # work well
    main(middle2) # StopIteration

I expect main(middle2) to work well as main(middle), but there is a StopIteration exception.

Upvotes: 1

Views: 748

Answers (2)

Blckknght
Blckknght

Reputation: 104712

The cause of the unexpected StopIteration exception in main is that your m.send(None) call causes your middle2 generator to be exhausted (after the sub-generator sales_sum breaks out of its loop in response to the falsey value it received). When a generator is exhausted, it raises StopIteration. Normally that's invisible because you consume iterators in for loops, but in this case, it breaks your code.

There are a few ways you could fix this. One would be to use a two-argument call to next instead of using m.send(None):

next(m, None)

This does the same thing as m.send(None), but has the added benefit of suppressing the StopIteration. Note that the None in the call to next is not really the same as the one in send. It's the default return value in the case of an exhausted iterator, not the value that gets sent in (which is always None when using next).

Another approach would be to change middle2 so that it doesn't end when the sales_sum generator does. You could add an extra yield statement at the end, so that it yields control one last time after doing its assignment to final_result when its sub-generator returns.

A final idea would be to replace m.send(None) with m.close(). This would require some changes in final_result, as the close call will throw a GeneratorExit exception into the generator. If you expect it, you could use that as your signal to be done instead of looking for a falsey value:

def sales_sum(pro_name):
    total = 0
    nums = []
    while True:
        try:
            x = yield
        except GeneratorExit:
            return total, nums

        print(pro_name+" Sales volume: ", x)
        total += x
        nums.append(x)

With this change, middle2 would not need any modification.

Upvotes: 1

chepner
chepner

Reputation: 531165

sales_sum is a finite iterator. middle2 iterates over it exactly once; middle tries to iterate over it multiple times.

Upvotes: 1

Related Questions