YellowPillow
YellowPillow

Reputation: 4270

Why can't I change the list I'm iterating from when using yield

I have some reproducible code here:

def test():
    a = [0, 1, 2, 3]
    for _ in range(len(a)):
        a.append(a.pop(0)) 
        for i in range(2,4):
            print(a)
            yield(i, a)

This prints out:

[1, 2, 3, 0]
[1, 2, 3, 0]
[2, 3, 0, 1]
[2, 3, 0, 1]
[3, 0, 1, 2]
[3, 0, 1, 2]
[0, 1, 2, 3]
[0, 1, 2, 3]

Which is what I expected, but when I do list(test()) I get:

[(2, [0, 1, 2, 3]),
 (3, [0, 1, 2, 3]),
 (2, [0, 1, 2, 3]),
 (3, [0, 1, 2, 3]),
 (2, [0, 1, 2, 3]),
 (3, [0, 1, 2, 3]),
 (2, [0, 1, 2, 3]),
 (3, [0, 1, 2, 3])]

Why is this the case, and what can I do to work around it?

Upvotes: 6

Views: 71

Answers (4)

Daniel H
Daniel H

Reputation: 7443

You end up with a list of tuples, and the second element of the tuple is the same list. You might notice that they are all equivalent to the last list the generator makes, not the first; the list is changing, but the tuples all have references to the same one.

To make this clear, try to modify one of the lists. For example, if you run l[0][1].append(4), you will get

[(2, [0, 1, 2, 3, 4]),
 (3, [0, 1, 2, 3, 4]),
 (2, [0, 1, 2, 3, 4]),
 (3, [0, 1, 2, 3, 4]),
 (2, [0, 1, 2, 3, 4]),
 (3, [0, 1, 2, 3, 4]),
 (2, [0, 1, 2, 3, 4]),
 (3, [0, 1, 2, 3, 4])]

All of the lists had 4 appended, because there is only one list.

If you want to return a copy, there are several ways. You could yield (i, a[:]) (using slice notation to get a copy), yield (i, list(a)) (using the list constructor to get a copy), or yield (i, copy.copy(a)) (using the copy module).

Upvotes: 1

developer_hatch
developer_hatch

Reputation: 16224

Explicit better implicit:

import copy
def test():
    a = [0, 1, 2, 3]
    for _ in range(len(a)):
        a.append(a.pop(0)) 
        for i in range(2,4):
            print(a)
            yield(i, copy.copy(a))

Upvotes: 1

Barmar
Barmar

Reputation: 781340

You're yielding the same list every time, so the caller's list simply has a bunch of references to that list, which has been updated each time they called. You need to make copies of the list when you yield:

def test():
    a = [0, 1, 2, 3]
    for _ in range(len(a)):
        a.append(a.pop(0)) 
        for i in range(2,4):
            print(a)
            yield(i, a[:])

Upvotes: 3

willeM_ Van Onsem
willeM_ Van Onsem

Reputation: 476813

Because you always return (i,a) now a is a reference to the list. So you constantly return a reference to the same list. This is no problem for the print statement, since it immediately prints the state of a at that moment.

You can return a copy of the list, for instance like:

def test():
    a = [0, 1, 2, 3]
    for _ in range(len(a)):
        a.append(a.pop(0)) 
        for i in range(2,4):
            print(a)
            yield(i, list(a))

Upvotes: 3

Related Questions