# [Tutor] Scope and values in a list

Daniel Yoo dyoo@hkn.eecs.berkeley.edu
Sun, 24 Dec 2000 18:52:07 -0800 (PST)

On Fri, 22 Dec 2000, Mallett, Roger wrote:

> I am having problems with a *for* loop picking up the *modified* value of a
> list and would therefore like some to better understand SCOPE.

[warning --- this message is long and wanders a bit]

Before we do anything, let's test something out:

###
>>> L1 = [1, 2, 3, 4]
>>> L2 = L1
>>> L2[0] = 'one'
>>> L2
['one', 2, 3, 4]
>>> L1
['one', 2, 3, 4]
###

Whenever we direct another variable to a preexisting list, it's as if we
are giving the list a different name or alias --- it's still the same
list, so when we make changes to the list, it'll look as if we modified
both of them.  Diagramically, it looks like this:

L1 -------> [1, 2, 3, 4]

^
L2 ---------|

On the other hand:

###
>>> L1 = [1, 2, 3, 4]
>>> L2 = L1
>>> L2 = ['one', 'two', 'three', 'four']
>>> L1
[1, 2, 3, 4]
>>> L2
['one', 'two', 'three', 'four']
###

Here we see that when we redirect L2 to a new list, L1 still points to the
old one.  This looks like the diagram:

L1 -------> [1, 2, 3, 4]

L2 -------> ['one', 'two', 'three', 'four']

With that background out of the way, let's try a few examples to see if we
can trigger your scoping problem.  First:

###  Case #1
>>> mylist = list(range(10))
>>> for x in mylist:
...     mylist.append(x)
...                         # waited for about 3 seconds, realized it was
# an infinite loop, and Ctrl-C'ed it
Traceback (innermost last):
File "<stdin>", line 2, in ?
KeyboardInterrupt
>>> len(mylist)
800885
###

So it does appear that you can modify a list in-place.  So that's probably
not what you did.

Let's try another example:

###  Case #2
>>> L = list(range(2))
>>> for x in L:
...   L = list(range(5))
...
>>> print L
[0, 1, 2, 3, 4]
###

So that also seems to work.  That doesn't trigger the behavior you
reported.  (At least, not at first glance.  In actuality, it does, but we
don't see it.)  Let's try something else.

### Case #3
>>> L = list(range(2))
>>> for x in L:
...     print x
...     L = list(range(len(L) * 2))
...
0
1
###

Ah ha!  There's that weird scoping problem.  We expected to see another
infinite loop, since L's length was supposed to double through each loop
iteration.  What's happening?

The statement "for x in L" is slightly special in the sense that Python
really keeps track of that list: you can think of it as Python giving the
list some sort of anonymous variable name, which it uses later to access
each list element.  So if we do something like this:

L = [1, 2, 3, 4]
for x in L:  ...

the diagram of what Python sees looks like this:

L ------------> [1, 2, 3, 4]
^
|
anonymous_name --|

After it gives an anonymous variable naming to the list, it'll grab each
element using the anonymous variable.  Why all this indirection?  This
model might seem a little awkward, but it helps to explain what happens in
the second and third cases.

In order to do a for loop, Python will try to grab elements until it hits
an IndexError.  In the first case, since we kept adding new elements to
the same list, we never hit rock bottom --- it keeps going.  We're making
internal changes to the same list, and that explains the first case's
infinite loop.

What's happening in the second?  Let's take a look again:

###  Case #2
>>> L = list(range(2))
>>> for x in L:
...   L = list(range(5))
...
>>> print L
[0, 1, 2, 3, 4]
###

Well, it's redirecting L to a new list.  What's crucial to notice is that
the for loop is still iterating through the list [0, 1] --- once it knows
which list to iterate over, it doesn't look at where L is redirected to.

And that's what the third case shows us:

### Case #3
>>> L = list(range(2))
>>> for x in L:
...     print x
...     L = list(range(len(L) * 2))
...
0
1
###

We see that the for loop goes through the elements [0, 1], but it doubles
the length of L each time through the loop.  In fact, let's print out L to
make sure that this is what just happened:

###
>>> L
[0, 1, 2, 3, 4, 5, 6, 7]
###

This might make more sense if we slightly reword what Python might do to
perform a for-loop:

###
for x in L:
... # body of the loop
###

could conceivable be rewritten as:

###
non_conflicting_list_name = L
non_conflicting_list_index = 0
try:
while 1:
x = non_conflicting_list_name[non_conflicting_list_index]
... # body of the loop
non_conflicting_list_index += 1
except IndexError: pass
###

*pant pant* Now THAT was long-winded.  The above might not even be how
Python does things, but this model explains what might be happening
underneath the surface.

Anyway, experiment with it a little more.  You might find a nicer way of
explaining what's happening --- it so, please post it up.  *grin*

Good luck!