3

I'm learning Python, coming from a C#/Java background, and was playing around with list behavior. I've read some of the documentation, but I don't understand how, or why, slices with indices larger than length-1 make is possible to append items.

ls = ["a", "b", "c", "d"]
n = len(ls)  # n = 4

letter = ls[4]  # index out of range (list indexes are 0 - 3)

# the following all do the same thing

ls += ["echo"]
ls.append("foxtrot")
ls[len(ls):] = ["golf"]  # equivalent to ls[len(ls): len(ls)]

print(ls)

Although it seems odd to me, I understand how slices can modify the list they operate on. What I don't get is why list[len(list)] results in the expected out of bounds error, but list[len(list):] doesn't. I understand that slices are fundamentally different than indexes, just not what happens internally when a slice begins with an index outside of the list values.

Why can I return a slice from a list that starts with a non-existent element (len(list))? And why does this allow me to expand the list?

Also, of the first three above methods for appending an item, which one is preferred from a convention or performance perspective? Are there performance disadvantages to any of them?

Brian H.
  • 2,092
  • 19
  • 39
  • I cannot explain what happens when you affect something to a list splicing (I don't even know why that is permitted). The preferred form is as usual in python the most readable, which would be either `ls += ["alpha"]` or `ls.append("tango")`. From a performance perspective, that depends on your python interpreter. – DainDwarf Jan 29 '16 at 12:59
  • Please narrow your question down; as it stands it's not clear whether it's a duplicate of http://stackoverflow.com/q/9490058/3001761 or of http://stackoverflow.com/q/10623302/3001761 or of something else. You could also look into this more yourself as part of refining it, there's lots of information around already. – jonrsharpe Jan 29 '16 at 13:01
  • Note that `ls +=` requires `ls` to be in the local scope (or given a `global` declaration in the function), since it's performing an assignment, but `ls.append` will work on globals since it's merely a method call on an existing mutable object, not an assignment. – PM 2Ring Jan 29 '16 at 13:10

2 Answers2

1

While a.append(x) and a[len(a):] = [x] do the same thing, the first is clearer and should almost always be used. Slice replacement may have a place, but you shouldn't look for reasons to do it. If the situation warrants it, it should be obvious when to do it.

Start with the fact that indexing returns an element of the list, and slicing returns a "sublist" of a list. While an element at a particular either exists or does not exist, it is mathematically convenient to think of every list have the empty list as a "sublist" between any two adjacent positions. Consider

>>> a = [1, 2, 3]
>>> a[2:2]
[]

Now consider the following sequence. At each step, we increment the starting index of the slice

>>> a[0:]   # A copy of the list
[1, 2, 3]
>>> a[1:]
[2, 3]
>>> a[2:]
[3]

Each time we do so, we remove one more element from the beginning of the list for the resulting slice. So what should happen when the starting index finally equals the final index (which is implicitly the length of the list)?

>>> a[3:]
[]

You get the empty list, which is consistent with the following identities

a + [] = a
a[:k] + a[k:] = a

(The first is a special case of the second where k >= len(a).

Now, once we see why it makes sense for the slice to exist, what does it mean to replace that slice with another list? In other words, what list do we get if we replace the empty list at the end of [1, 2, 3] with the list [4]? You get [1, 2, 3, 4], which is consistent with

[1, 2, 3] + [4] = [1, 2, 3, 4]

Consider another sequence, this time involving slice replacements.

>>> a = [1,2,3]; a[0:] = ['a']; a
['a']
>>> a = [1,2,3]; a[1:] = ['a']; a
[1, 'a']
>>> a = [1,2,3]; a[2:] = ['a']; a
[1, 2, 'a']
>>> a = [1,2,3]; a[3:] = ['a']; a
[1, 2, 3, 'a']

Another way of looking at slice replacement is to treat it as appending a list to a sublist of the original. When that sublist is the list itself, then it is equal to appending to the original.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • Great explanation. Thinking of the slice as occurring between elements (or the element and the end of the list) makes the operations understandable to me. – Brian H. Jan 29 '16 at 14:12
0

Why can I return a slice from a list that starts with a non-existent element?

I can't tell for sure, but my guess would be that it's because a reasonable value can be returned (there are no elements this refers to, so return an empty list).

And why does this allow me to expand the list?

Your slice selects the last 0 elements of the list. You then replace those elements with the elements of another list.

Also, of the first three above methods for appending an item, which one is preferred

Use .append() if you're adding a single element, and .extend() if you have elements inside another list.

I'm not sure about performance, but my guess would be that those are pretty well optimized.

stranac
  • 26,638
  • 5
  • 25
  • 30
  • "Your slice selects the last 0 elements of the list. You then replace those elements with the elements of another list." I guess that's the part I can't wrap my head around. Selecting "nothing" and replacing it with "something". I get how you might select the last element and replace it with [last_elem, new_elem], but that isn't what is happening. Too meta-physical probably. Weird behavior, but nothing I can't work around. I just don't like not knowing how it works. – Brian H. Jan 29 '16 at 13:10