I was doing a simple exercise to understand the mechanics of List assignment. If I assign a list L1 to L2 and then append an element to L1, both L1 and L2 are changed. However if I assign a list L3 to a subslice of L2 and then append an element to L3, the changes in L3 are disconnected from L2. How is this accomplished? L3 actually points to a different location now which contains a subslice of the list while L1 and L2 point to the same location. Is that right?
>>> L1 = []
>>> L2 =[1,2,3]
>>> L1 = L2
>>> L1.append(4)
>>> L1
[1, 2, 3, 4]
>>> L2
[1, 2, 3, 4]
>>> L3 =L2[:2]
>>> L3
[1, 2]
>>> L3.append(5)
>>> L3
[1, 2, 5]
>>> L2
[1, 2, 3, 4]
3 Answers 3
The answer is simple: A an assignment assigns a reference. That’s why changes to L1 are visible in L2 – they’re the same object.
A slice, however, creates a (shallow) copy of the range. Hence changes to L3 are disconnected from L2.
In fact, in order to create a copy of a sequence, since you cannot use direct assignment, you can use a slice instead:
>>> L4 = L2[:]
>>> L4.append(5)
>>> L2
[1, 2, 3, 4]
... however, that’s more commonly done via the constructor, i.e. L4 = list(L2).
3 Comments
(I wasted too much time in examples and @Konrad Rudolph beat me to the answer, I'm basically saying the same thing as him)
Python is doing the equivalent of C pointer assignment under the hood, L1 held the address for a list in memory, and L2 just copied the same address.
You can check in code, simply using the is operator.
>>> L1 is L2
True
When using slices though, a new list must be created, since it will differ from the original one, but python, being the sneaky bastard that it is, instead of copying everything, it just copies the references to the objects inside.
Here's a more elaborate example:
>>> L1 = [1, [2, 3], 4]
>>> L2 = L1
>>> L2
[1, [2, 3], 4]
>>> L1[1] is L2[1]
True
Ok, so L1 holds a list with a list inside, you could replace this with a dictionary, string or any other python object. Notice that the is operator returned True for the list inside the list.
Then, if you do the slicing trick:
>>> L3 = L1[:2]
>>> L3
[1, [2, 3]]
>>> L3 is L1
False
>>> L3[1] is L2[1]
True
The outside list has changed, but the objects within remain the same, this is known as shallow copying. In fact, the same rules keep applying, to the point if that we add a new item to the list within the list:
>>> L3[1].append(9)
>>> L3
[1, [2, 3, 9]]
>>> L1
[1, [2, 3, 9], 4]
(notice the added 9) The list inside of both L1 and L3 are changed.
By contrast, you can do deepcopy when shallow copying is not desired:
>>> from copy import deepcopy
>>> L3 = deepcopy(L1)
>>> L3
[1, [2, 3, 9], 4]
>>> L3 is L1
False
Comments
To add to the answer above, the nature of the copy becomes important when you have a list of mutable objects, instead of integers or strings. The elements of the lists still point to the same objects even when you slice.
>>> a = { 'a': 0 }
>>> b = { 'b' : 0 }
>>> c = { 'c' : 0 }
>>> l = [ a, b, c ]
>>> m = l[ : 2]
m and l contain references to the same things.
>>> m
[{'a': 0}, {'b': 0}]
>>> m[0]['a'] = 1
>>> m
[{'a': 1}, {'b': 0}]
>>> l
[{'a': 1}, {'b': 0}, {'c': 0}]
However, m and l are different things.
>>> m[0] = {'d': 1}
>>> m
[{'d': 1}, {'b': 0}]
>>> l
[{'a': 1}, {'b': 0}, {'c': 0}]
slicefrom a list creates a shallow copy...L2. You're thinking of the shallow copy as deep because the thing you're thinking of as a shallow copy - direct assignment - is not actually a copy at all. This is a fundamental difference between languages like C++, where variables are places to put things and assignment creates copies, and languages like Python, where variables are names for things, and assignment just moves nametags around.