Dschoni Dschoni - 4 months ago 17
Python Question

Changing iterable variable during loop

Let

it
be an iterable element in python.
In what cases is a change of
it
inside a loop over
it
reflected? Or more straightforward: When does something like this work?

it = range(6)
for i in it:
it.remove(i+1)
print i


Leads to 0,2,4 being printed (showing the loop runs 3 times).

On the other hand does

it = range(6)
for i in it:
it = it[:-2]
print it


lead to the output:

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


showing the loop runs 6 times. I guess it has something to do with in-place operations or variable scope but cannot wrap my head around it 100% sure.

Clearification:

One example, that doesn't work:

it = range(6)
for i in it:
it = it.remove(i+1)
print it


leads to 'None' being printed and an Error (NoneType has no attribute 'remove') to be thrown.

Answer

When you iterate over a list you actually call list.__iter__(), which returns a listiterator object bound to the list, and then actually iterate over this listiterator. Technically, this:

itt = [1, 2, 3]
for i in itt:
    print i

is actually kind of syntactic sugar for:

itt = [1, 2, 3]
iterator = iter(itt)
while True:
    try:
        i  = it.next()
    except StopIteration:
        break
    print i

So at this point - within the loop -, rebinding itt doesn't impact the listiterator (which keeps it's own reference to the list), but mutating itt will obviously impact it (since both references point to the same list).

IOW it's the same old difference between rebinding and mutating... You'd get the same behaviour without the for loop:

# creates a `list` and binds it to name "a"
a = [1, 2, 3] 
# get the object bound to name "a" and binds it to name "b" too.
# at this point "a" and "b" both refer to the same `list` instance
b = a 
print id(a), id(b)
print a is b
# so if we mutate "a" - actually "mutate the object bound to name 'a'" - 
# we can see the effect using any name refering to this object:
a.append(42)
print b

# now we rebind "a" - make it refer to another object
a = ["a", "b", "c"]
# at this point, "b" still refer to the first list, and
# "a" refers to the new ["a", "b", "c"] list
print id(a), id(b)
print a is b
# and of course if we now mutate "a", it won't reflect on "b"
a.pop()
print a
print b