6

I know that Slicing lists does not generate copies of the objects in the list; it just copies the references to them.

But if that's the case, then why doesn't this work?

l = [1, 2, 3]

# Attempting to modify the element at index 1
l[0:2][-1] = 10

# but the attempt fails. The original list is unchanged
l
> [1, 2, 3]

Shouldn't l[0:2][-1] point to the element at index 1 of the original list?

Heisenberg
  • 8,386
  • 12
  • 53
  • 102
  • You can't modify the element itself; it's immutable. Assigning to an element of a list replaces one reference with another. – chepner May 03 '20 at 19:20
  • 1
    Also, there is a differnce between "copying the list" (shallow copy) and "copying the objects of the list" (deep copy). – chepner May 03 '20 at 19:20

3 Answers3

4

Slicing a list returns a new shallowly-copied list object. While you are correct that it does not deep-copy the original list's items, the result is a brand new list distinct from the original.

See the Python 3 tutorial:

All slice operations return a new list containing the requested elements. This means that the following slice returns a shallow copy of the list:

>>> squares = [1, 4, 9, 16, 25]
>>> squares[:]
[1, 4, 9, 16, 25]

Consider

>>> squares[:] is squares
False
obataku
  • 29,212
  • 3
  • 44
  • 57
  • 1
    Even though the slice is a new list, it still contains the reference to the old objects. (That's what I think "shallow copy" means.) That makes sense, given that `l = [1,2,3]; l[0:1] = [10]` does modify the list. However, `l[0:1][0] = [10]` does not modify the list. Why are `l[0:1]` and `l[0:1][0]` different? Shouldn't they both refer to the same object, i.e. the first item of `l`? – Heisenberg May 03 '20 at 21:01
  • 3
    @Heisenberg `l[0:1] = [10]` is not constructing a new `list` from a `slice`--it is assigning to the existing `list` using a `slice`. on the other hand, `l[0:1][0] = 10` is *first* constructing a new `list` using a `slice` and *then* assigning to its first element using an simple index (`0`). – obataku May 04 '20 at 21:05
  • 1
    the distinction is that `l[0:1] = [10]` is equivalent to `l.__setitem__(slice(0, 1), [10])` whereas `l[0:1][0] = 10` is akin to `l.__getitem__(slice(0, 1)).__setitem__(0, 10)` – obataku May 04 '20 at 21:09
  • 2
    in the first instance, you are calling `__setitem__` on `l` and thus mutating the original list; in the second, however, you are mutating the newly-created `list` from `l.__getitem__`. to more clearly see what is happening, consider introducing an intermediate variable like so: `l2 = l[0:1]; l2[0] = 10`. inspecting `l2` afterward you will notice `l2 == [10]` with `l` is unchanged – obataku May 04 '20 at 21:13
  • @Heisenberg: I added an answer that may clarify matters. The main point is that when you assign to a list index you are not affecting the object in the list; you are only modifying the list itself (so you are modifying the new list that your slice created). – BrenBarn May 05 '20 at 03:12
3

You are right that slicing doesn't copy the items in the list. However, it does create a new list object.

Your comment suggests a misunderstanding:

# Attempting to modify the element at index 1
l[0:2][-1] = 10

This is not a modification of the element, it's a modification of the list. In other words it is really "change the list so that index 1 now points to the number 10". Since your slice created a new list, you are just changing that new list to point at some other object.

In your comment to oldrinb's answer, you said:

Why are l[0:1] and l[0:1][0] different? Shouldn't they both refer to the same object, i.e. the first item of l?

Aside from the fact that l[0:1] is a list while l[0:1][0] is a single element, there is again the same misunderstanding here. Suppose that some_list is a list and the object at index ix is obj. This:

some_list[ix] = blah

. . . is an operation on some_list. The object obj is not involved. This can be confusing because it means some_list[ix] has slightly different semantics depending on which side of the assignment it is on. If you do

blah = some_list[ix] + 2

. . .then you are indeed operating on the object inside the list (i.e., it is the same as obj + 2). But when the indexing operation is on the left of the assignment, it no longer involves the contained object at all, only the list itself.

When you assign to a list index you are modifying the list, not the object inside it. So in your example l[0] is the same as l[0:2][0], but that doesn't matter; because your indexing is an assignment target, it's modifying the list and doesn't care what object was in there already.

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
1

For the sake of explaining it better, assume you had written

l = [1, 2, 3]
k = l[0:2]
k[-1] = 10

I hope you can agree that this is equivalent.

Now let's break down the individual statements:

l = [1, 2, 3]

This creates the following objects and references:

id  object
--  --------
0   <int 1>
1   <int 2>
2   <int 3>
3   <list A>
name  →  id
----     --
l     →  3
l[0]  →  0
l[1]  →  1
l[2]  →  2

k = l[0:2]

This creates a new list <list B> containing copies of the references contained in l:

id  object
--  --------
0   <int 1>
1   <int 2>
2   <int 3>
3   <list A>
4   <list B>
name  →  id
----     --
l     →  3
l[0]  →  0
l[1]  →  1
l[2]  →  2
k     →  4
k[0]  →  0  (copy of l[0])
k[1]  →  1  (copy of l[1])

k[-1] = 10

First, index −1 resolves to index 1 (because k has length 2), so this is equivalent to k[1] = 10. This assignment means that the objects and references are updated as such:

id  object
--  --------
0   <int 1>
1   <int 2>
2   <int 3>
3   <list A>
4   <list B>
5   <int 10>
name  →  id
----     --
l     →  3
l[0]  →  0
l[1]  →  1
l[2]  →  2
k     →  4
k[0]  →  0
k[1]  →  5

Note how l and l[0] to l[2] are not affected by this. QED.

mkrieger1
  • 19,194
  • 5
  • 54
  • 65