Assignment would seem to be the most elementary programming concept, not deserving a separate discussion. However, there are some surprising subtleties here. Consider the following code fragment:


This behaves exactly as expected. When we write bar = foo in the code O, the value of foo (the string 'Monty') is assigned to bar. That is, bar is a copy of foo, so when we overwrite foo with a new string 'Python' on line ©, the value of bar is not affected.

However, assignment statements do not always involve making copies in this way. Assignment always copies the value of an expression, but a value is not always what you might expect it to be. In particular, the "value" of a structured object such as a list is actually just a reference to the object. In the following example, O assigns the reference of foo to the new variable bar. Now when we modify something inside foo on line ©, we can see that the contents of bar have also been changed.

>>> foo = ['Monty', 'Python'] >>> bar = foo >>> foo[1] = 'Bodkin' 0 >>> bar

The line bar = foo O does not copy the contents of the variable, only its "object reference." To understand what is going on here, we need to know how lists are stored in the computer's memory. In Figure 4-1, we see that a list foo is a reference to an object stored at location 3133 (which is itself a series of pointers to other locations holding strings). When we assign bar = foo, it is just the object reference 3133 that gets copied. This behavior extends to other aspects of the language, such as parameter passing (Section 4.4).

Figure 4-1. List assignment and computer memory: Two list objects foo and bar reference the same location in the computer's memory; updating foo will also modify bar, and vice versa.

Let's experiment some more, by creating a variable empty holding the empty list, then using it three times on the next line. >>> empty = []

>>> nested[1].append('Python') >>> nested

Observe that changing one of the items inside our nested list of lists changed them all. This is because each of the three elements is actually just a reference to one and the same list in memory.

Your Turn: Use multiplication to create a list of lists: nested = [[]] * 3. Now modify one of the elements of the list, and observe that all the elements are changed. Use Python's id() function to find out the numerical identifier for any object, and verify that id(nested[0]), id(nested[1]), and id(nested[2]) are all the same.

Now, notice that when we assign a new value to one of the elements of the list, it does not propagate to the others:

>>> nested = [[]] * 3 >>> nested[1].append('Python') >>> nested[1] = ['Monty'] >>> nested

We began with a list containing three references to a single empty list object. Then we modified that object by appending 'Python' to it, resulting in a list containing three references to a single list object ['Python']. Next, we overwrote one of those references with a reference to a new object ['Monty']. This last step modified one of the three object references inside the nested list. However, the ['Python'] object wasn't changed, and is still referenced from two places in our nested list of lists. It is crucial to appreciate this difference between modifying an object via an object reference and overwriting an object reference.

Python provides two ways to check that a pair of items are the same. The is operator tests for object identity. We can use it to verify our earlier observations about objects. First, we create a list containing several copies of the same object, and demonstrate that they are not only identical according to ==, but also that they are one and the same object:

>>> snake_nest[0] == snake_nest[1] == snake_nest[2] == snake_nest[3] == snake_nest[4] True

>>> snake_nest[0] is snake_nest[1] is snake_nest[2] is snake_nest[3] is snake_nest[4] True

Now let's put a new python in this nest. We can easily show that the objects are not all identical:

>>> position = random.choice(range(size)) >>> snake_nest[position] = ['Python'] >>> snake_nest

[['Python'], ['Python'], ['Python'], ['Python'], ['Python']]

>>> snake_nest[0] == snake_nest[1] == snake_nest[2] == snake_nest[3] == snake_nest[4] True

>>> snake_nest[0] is snake_nest[1] is snake_nest[2] is snake_nest[3] is snake_nest[4] False

You can do several pairwise tests to discover which position contains the interloper, but the id() function makes detection is easier:

>>> [id(snake) for snake in snake_nest] [513528, 533168, 513528, 513528, 513528]

This reveals that the second item of the list has a distinct identifier. If you try running this code snippet yourself, expect to see different numbers in the resulting list, and don't be surprised if the interloper is in a different position.

Having two kinds of equality might seem strange. However, it's really just the type-token distinction, familiar from natural language, here showing up in a programming language.

Important: To copy the items from a list foo to a new list bar, you can write bar = foo[:]. This copies the object references inside the list. To f $ copy a structure without copying any object references, use copy.deep c°py().

Was this article helpful?

0 0

Post a comment