-1

My Question is:

Why it works for the inner loop, but not the outer one?

NOT

how to write something to do this job... I know it is not a safe solution to change list dynamically in loop.


Code 1:

list_a = [1,2,3]
list_b = [11,12,13]

for i in list_a:
    for j in list_b:
        print(i, j)

Result 1:

1 11
1 12
1 13
2 11
2 12
2 13
3 11
3 12
3 13

Code 1 is a two-level nested loop for printing combination of two list, and it works as expected.

I want to change the list dynamically during the loop. By changing the list that trigger the looping, I expect the looping behavior is also changed dynamically.


Code 2:

list_a = [1,2,3]
list_b = [11,12,13]

for i in list_a:
    list_a = [100, 200]
    for j in list_b:
        print(i, j)

Code 3:

list_a = [1,2,3]
list_b = [11,12,13]

for i in list_a:
    for j in list_b:
        list_a = [100, 200]
        print(i, j)

Result 2/3:

1 11
1 12
1 13
2 11
2 12
2 13
3 11
3 12
3 13

Code 2 and Code 3 is for changing the first list. Although list_a is updated in the first step of the loop, the looping behavior of outer for loop dose not change.


Code 4:

list_a = [1,2,3]
list_b = [11,12,13]

for i in list_a:
    for j in list_b:
        list_b = [100, 200]
        print(i, j)

Result 4:

1 11
1 12
1 13
2 100
2 200
3 100
3 200

Code 4 is for changing the second list. list_b is updated in the first step of the loop, and the looping behavior of the inner one is affected.

Chang Ye
  • 1,105
  • 1
  • 12
  • 25
  • 3
    Quoting [Alex Martelli](https://stackoverflow.com/users/95810/alex-martelli) from the answer [here](https://stackoverflow.com/questions/1637807/modifying-list-while-iterating), *Never alter the container you're looping on, because iterators on that container are not going to be informed of your alterations and, as you've noticed, that's quite likely to produce a very different loop and/or an incorrect one.* – fabio.avigo Oct 05 '18 at 12:01
  • To add to what fabio said: When you create a `for` loop, an iterator is created from the iterable that you're looping over, that is `for i in list_a` is equivalent to doing `it = iter(list_a)` `while True:` `try:` `i = next(it)` `except StopIteration: break` – PM 2Ring Oct 05 '18 at 12:06
  • @PM2Ring that is weird... why it works for changing list_b? – Chang Ye Oct 05 '18 at 12:09
  • 2
    Notice it doesn't work when `i==1`, it's still using the old `b`. But it works on the next two runs because `b` got replaced before the `for j in list_b:` made a new iterator. – PM 2Ring Oct 05 '18 at 12:12
  • I suspect the root issue here is that you're confused about how assignment statements work in Python. I would recommend reading Ned Batchelder's [Facts and myths about Python names and values](https://nedbatchelder.com/text/names.html) which will likely be enlightening. – Daniel Pryden Oct 05 '18 at 12:13
  • thank you @DanielPryden, I will read the post and find out why. It is very helpful. – Chang Ye Oct 05 '18 at 12:15
  • @DanielPryden I wouldn't mind a dollar for every time I've linked that article. :) And it's definitely required reading, but I think the main issue here is that it's not immediately obvious that `__iter__` and `__next__` are involved in `for` loops, and what they do. – PM 2Ring Oct 05 '18 at 12:17
  • 1
    Or perhaps a better way to say it: you are not "dynamically changing" *any* list in any of your loops in the question. Instead, you are creating *new* lists, which sometimes will get used and sometimes won't, depending on how the old lists were being used. – Daniel Pryden Oct 05 '18 at 12:18

2 Answers2

2

The problem is that for loops create and bind their iterators on entry, and it doesn't matter if you rebind the name they came from later. Imagine a for loop of this form:

for i in list_a:

as being implemented like this:

_unnamed_iter_ = iter(list_a)
while True:
    try:
       i = next(_unnamed_iter_)
    except StopIteration:
       break

Notice how, during the loop, it never looks at list_a; it reads from an iterator backed by whatever was bound to list_a before the loop began, but reassigning list_a to a new list, as opposed to modifying the list it's currently bound to, won't change what the iterator is looking at. Modifying the currently bound list would "work", though it's explicitly against the rules (modifying collections while iterating them sometimes works, but is usually a source of subtle bugs). An easy way to make your code work as you expect is to change:

list_a = [100, 200]

to:

list_a[:] = [100, 200]

Slice assignment replaces the contents of the list bound to list_a, it doesn't rebind it to a brand new list, so the iterator that is backed by the original list is modified as expected.

Code 4 kind of works because you loop over list_b multiple times (as in you begin looping more than once, and a new iterator is created from "whatever is bound to list_b" each time you begin the loop). You'll note the reassignment didn't change the first loop over list_b (because the iterator continued to use the old list), but on the second loop (with the second value from list_a) it creates a new iterator, which is based on the newly bound value of list_b.

In any event, this is all pointless navel gazing. None of what you're doing is safe, or sane, and the "fix" (slice assignment) is highly likely to break in other Python interpreters (and possibly in future versions of your interpreter).

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
1

Python scoping rules

Python has wide scopes. Contrarily to some languages that create a new scope for every if-statement, for-statement, etc., Python only has three layers of scoping: the local scope, its closure and the global scope.

# global scope
a = 1

def f():
    # closure of g
    b = 2

    def g():
        # local scope of g
        c = 3
        print(
            a, # get a from the global scope
            b, # get b from the closure
            c  # get c from the local scope
        )

    g()

f() # prints: 1 2 3

Other languages scoping rules

By example, consider this JavaScript snippet.

// JavaScript

let x = [1, 2]

for (i = 0; i < 3; i++) {
    let x = [3, 4] // The use of let declares are new variable specific to this scope
    console.log(x) // [3, 4]
}

console.log(x) // [1, 2]

What we notice here is that a for-loop has its own scope. This is the case for many well-known languages. Although, the same does not happen in Python.

# Python3

x = [1, 2]
for i in x:
    x = [3, 4]
    print(x) # [3, 4]

print(x) # [3, 4]

Notice how the assignement of x inside the loop leaked out of the loop.

How does that apply

Let's have a look back at your example. All of the code below happens in the same scope.

list_a = [1 ,2, 3]
list_b = [11, 12, 13]

for i in list_a:
    for j in list_b:
        list_b = [100, 200] # This makes the name 'list_b' point to the value [100, 200]
        print(i, j)

    # Here the name 'list_b' is not set back to [11, 12, 13] since we are in the same scope

Since everything happens in the same scope, on the second iteration over list_a, the name list_b now points to a new value: [100, 200].

How do we go around that?

Due to how scoping works in Python, it is not only bad practice to alter an iterable as we are iterating over it, but also bad practice to reuse its name. Thus you want to use new names for variables defined inside a for-loop as those will leak out of the loop.

list_a = [1 ,2, 3]
list_b = [11, 12, 13]

current_iterable = list_b
for i in list_a:
    for j in current_iterable:
        print(i, j)
    current_iterable = [100, 200]

print(list_b) # [11, 12, 3]
print(current_iterable) = [100, 200]

As you can see, the current_iterable name still exists outside of the loop, but at least we did not overwrite the already exising names with values which were meant to be used only by the loop.

This is of course a rule of thumb, sometimes we may want to incrementaly update an iterable.

def breadth_first_search(initial_node):
    nodes = [initial_node]

    for node in nodes:
        node.seen = True
        queue.extend(n for n in node.neighbors if not n.seen)

    return nodes
Olivier Melançon
  • 21,584
  • 4
  • 41
  • 73