23

I thought I understood the basics of list slicing in python, but have been receiving an unexpected error while using a negative step on a slice, as follows:

>>> a = list(range(10))
>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> a[:-1]
[0, 1, 2, 3, 4, 5, 6, 7, 8]
>>> a[::-1]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
>>> a[:-1:-1]
[]

(Note that this is being run in Python 3.5)

Why doesn't a[:-1:-1] reverse step through the a[:-1] slice in the same manner as it does through the whole list with a[::-1]?

I realize that you can use list.reverse() as well, but trying to understand the underlying python slice functionality better.

Matt Kelty
  • 241
  • 1
  • 2
  • 6
  • 1
    I think you're looking for ``a[-1::-1]``. The first index gives the start, the second index gives the end, and you want to start at index -1. – jakevdp Jan 02 '17 at 17:17
  • @ekhumoro: I did, and it works. `[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]` – ash Jan 02 '17 at 17:26
  • 1
    @Josh. How is that different to `a[::-1]`? – ekhumoro Jan 02 '17 at 17:28
  • @ekhumoro: good point, `a[-2::-1]` (produces `[8, 7, 6, 5, 4, 3, 2, 1, 0]`) is probably actually the answer, since the starting index is included. – ash Jan 02 '17 at 17:30
  • @Josh, you're right `a[-2::-1]` is the solution I was looking for, but I didn't understand it fully until the discussion below about how the +/- sign of the step impacted the behavior of the ends. – Matt Kelty Jan 02 '17 at 18:10

6 Answers6

33

The first -1 in a[:-1:-1] doesn't mean what you think it does.

In slicing, negative start/end indices are not interpreted literally. Instead, they are used to conveniently refer to the end of the list (i.e. they are relative to len(a)). This happens irrespectively of the direction of the slicing.

This means that

a[:-1:-1]

is equivalent to

a[:len(a)-1:-1]

When omitted during reverse slicing, the start index defaults to len(a)-1, making the above equivalent to

a[len(a)-1:len(a)-1:-1]

This always gives an empty list, since the start and end indices are the same and the end index is exclusive.

To slice in reverse up to, and including, the zeroth element you can use any of the following notations:

>>> a[::-1]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
>>> a[:None:-1]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
>>> a[:-len(a)-1:-1]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
NPE
  • 486,780
  • 108
  • 951
  • 1,012
  • How would you explain the difference between the results of `a[::-1]`, `a[:0:-1]`, and `a[:-1:-1]`? Or to put it another way: what actual number is represented by the empty end value? – ekhumoro Jan 02 '17 at 17:49
  • I think `-len(a)-1` can be used to represent the end (`j`, in `a[i:j:k]`). If `a` is `range(10)`, then `a[:-len(a)-1:-1]` produces `[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]`. Here, `-len(a)-1` evaluates to -11; this is negative, so `len(a) + -11` is used instead ([note 3](https://docs.python.org/3/library/stdtypes.html#common-sequence-operations)), which evaluates to -1. – ash Jan 02 '17 at 18:33
12

When you type [1, 2, 3, ...][1:4:1] it is the same as [1, 2, 3, ...][slice(1, 4, 1)]. So 1:4:1 is the shorthand for slice object. slice signature is slice(stop) or slice(start, stop[, step]) and you can also use None for arguments.

:: -> slice(None, None, None)
:4 -> slice(4)
# and so on

Suppose we have got [a: b: c]. Rules for indices will be as follows:

  1. First c is checked. Default is +1, sign of c indicates forward or backward direction of the step. Absolute value of c indicates the step size.
  2. Than a is checked. When c is positive or None, default for a is 0. When c is negative, default for a is -1.
  3. Finally b is checked. When c is positive or None, default for b is len. When c is negative default for b is -(len+1).

Note 1: Degenerated slices in Python are handled gracefully:

  • the index that is too large or too small is replaced with len or 0.
  • an upper bound smaller than the lower bound returns an empty list or string or whatever else (for positive c).

Note 2: Roughly speaking, Python picks up elements while this condition (a < b) if (c > 0) else (a > b) is True (updating a += c on every step). Also, all negative indices are replaced with len - index.

If you combine this rules and notes it will make sense why you got an empty list. In your case:

 In[1]: [1, 2, 3, 4, 5, 6][:-1:-1]        # `c` is negative so `a` is -1 and `b` is -1
Out[1]: [] 

# it is the same as:

 In[2]: [1, 2, 3, 4, 5, 6][-1: -1: -1]    # which will produce you an empty list 
Out[2]: [] 

There is very good discussion about slice notation: Explain Python's slice notation!

Community
  • 1
  • 1
godaygo
  • 2,215
  • 2
  • 16
  • 33
4

I generally find it useful to slice a range-object (this is only possible in python3 - in python2 range produces a list and xrange can't be sliced) if I need to see which indices are used for a list of a given length:

>>> range(10)[::-1]  
range(9, -1, -1)

>>> range(10)[:-1]  
range(0, 9)

And in your last case:

>>> range(10)[:-1:-1]
range(9, 9, -1)

This also explains what happened. The first index is 9, but 9 isn't lower than the stop index 9 (note that in python the stop index is excluded) so it stops without giving any element.

Note that indexing can also be applied sequentially:

>>> list(range(10))[::-1][:-1]  # first reverse then exclude last item.
[9, 8, 7, 6, 5, 4, 3, 2, 1]
>>> list(range(10))[:-1][::-1]  # other way around
[8, 7, 6, 5, 4, 3, 2, 1, 0]
MSeifert
  • 145,886
  • 38
  • 333
  • 352
3

Python's slices seem fairly simple at first, but their behaviour is actually quite complex (notes 3 and 5 are relevant here). If you have a slice a[i:j:k]:

  • If i or j are negative, they refer to an index from the end of a (so a[-1] refers to the last element of a)
  • If i or j are not specified, or are None, they default to the ends of a, but which ends depends on the sign of k:

    • if k is positive, you're slicing forwards, so i becomes 0 and j becomes len(a)
    • if k is negative, you're slicing backwards, so i becomes len(a) and j becomes the element before the start of a.

      NB: j cannot be replaced with -1, since doing that will cause Python to treat j as the last element of a rather than the (nonexistent) element before a[0]. To get the desired behaviour, you must use -len(a)-1 (or -(len(a)+1)) in place of j, which means that to get to a[j], slice starts at the last element of a, goes left for len(a) elements and then left one more element, ending up before a starts and thus including a[0] in the slice.

Therefore, a[:-1:-1] means "go from the end of a, which is a[-1] (since i is unspecified and k is negative), to the last element of a (since j == -1), with step size of -1". i and j are equal – you start and stop slicing in the same place – so the expression evaluates to an empty list.

To reverse a[:-1], you can use a[-2::-1]. This way, the slice starts at the penultimate element, a[-2] (since a[:-1] does not include a[-1]) and goes backwards until the element "before" a[0], meaning that a[0] is included in the slice.

>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> a[:-1]
[0, 1, 2, 3, 4, 5, 6, 7, 8]
>>> a[-2::-1]
[8, 7, 6, 5, 4, 3, 2, 1, 0]
ash
  • 5,139
  • 2
  • 27
  • 39
  • 2
    "`a[:-1:-1]` means that you're starting at the beginning (index 0) and going backwards until you hit the last element (index -1)." Not true. See note #5 under the [docs for sequence operations](https://docs.python.org/3/library/stdtypes.html#common-sequence-operations): In the slice `a[i:j:k]`, "If i or j are omitted or None, they become “end” values (which end depends on the sign of k)." That's why `a[:1:-1]` gives the last `len(a) - 2` elements, not the first element; it's equivalent to `a[len(a)-1:1:-1]`. – ThisSuitIsBlackNot Jan 02 '17 at 17:48
  • 1
    @ThisSuitIsBlackNot I think this is the key I didn't grasp: "which end depends upon the sign of k". Thank you! – Matt Kelty Jan 02 '17 at 17:55
  • @ThisSuitIsBlackNot, Matt: you're totally right, I'm sorry I got this so wrong. I'll remove the incorrect information and rewrite this. – ash Jan 02 '17 at 18:11
1

slice works similar to range in that when you make the step argument a negative number, the start and stop arguments work in the opposite direction.

>>> list(range(9, -1, -1)) == a[::-1]
True

Some examples that may help make this more clear:

>>> a[6:2:-2]
[6, 4]
>>> a[0:None:1] == a[::]
True
>>> a[-1:None:-1] == a[::-1]
True
>>> a[-2:None:-1] == a[:-1][::-1]
True
John B
  • 3,566
  • 1
  • 16
  • 20
0

In a simple way understand that if a[::-1] -1 at the end reverses the string.

Now

a=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
a[::-1]=[9, 8, 7, 6, 5, 4, 3, 2, 1, 0];

now a[:-1:-1] -1 in the middle doesn't make any sense because now it's the first element and this will give an empty list. Whereas a[-1::-1] gives you completed list and makes sense.

Maximouse
  • 4,170
  • 1
  • 14
  • 28
  • 1
    Please clarify your answer. Saying **doesn't make any sens** or **makes sense** is not an explanation. – Erich Apr 16 '20 at 11:24