14

I understand that given an iterable such as

>>> it = [1, 2, 3, 4, 5, 6, 7, 8, 9]

I can turn it into a list and slice off the ends at arbitrary points with, for example

>>> it[1:-2]
[2, 3, 4, 5, 6, 7]

or reverse it with

>>> it[::-1]
[9, 8, 7, 6, 5, 4, 3, 2, 1]

or combine the two with

>>> it[1:-2][::-1]
[7, 6, 5, 4, 3, 2]

However, trying to accomplish this in a single operation produces in some results that puzzle me:

>>> it[1:-2:-1] 
[]
>>>> it[-1:2:-1] 
[9, 8, 7, 6, 5, 4]
>>>> it[-2:1:-1]
[8, 7, 6, 5, 4, 3]

Only after much trial and error, do I get what I'm looking for:

>>> it[-3:0:-1]
[7, 6, 5, 4, 3, 2]

This makes my head hurt (and can't help readers of my code):

>>> it[-3:0:-1] == it[1:-2][::-1]
True

How can I make sense of this? Should I even be pondering such things?


FWYW, my code does a lot of truncating, reversing, and listifying of iterables, and I was looking for something that was faster and clearer (yes, don't laugh) than list(reversed(it[1:-2])).

orome
  • 45,163
  • 57
  • 202
  • 418

5 Answers5

7

This is because in a slice like -

list[start:stop:step]

start is inclusive, resultant list starts at index start.

stop is exclusive, that is the resultant list only contains elements till stop - 1 (and not the element at stop).

So for your caseit[1:-2] - the 1 is inclusive , that means the slice result starts at index 1 , whereas the -2 is exclusive , hence the last element of the slice index would be from index -3.

Hence, if you want the reversed of that, you would have to do it[-3:0:-1] - only then -3 would be included in the sliced result, and the sliced result would go upto 1 index.

nneonneo
  • 171,345
  • 36
  • 312
  • 383
Anand S Kumar
  • 88,551
  • 18
  • 188
  • 176
  • I guess what this means is that "stride" really wasn't in the minds of the designers when they though about slice syntax (which has some conveniences, to be sure). – orome Oct 26 '15 at 17:48
  • 1
    Yea, I believe this could be a good read for that, not completely related to slicing, but does mention that - http://python-history.blogspot.in/2013/10/why-python-uses-0-based-indexing.html – Anand S Kumar Oct 26 '15 at 17:51
6

The important things to understand in your slices are

  • Start will be included in the slice

  • Stop will NOT be included in the slice

  • If you want to slice backwards, the step value should be a negative value.

Basically the range which you specify is a half-open (half-closed) range.


When you say it[-3:0:-1] you are actually starting from the third element from the back, till we reach 0 (not including zero), step one element at a time backwards.

>>> it[-3:0:-1]
[7, 6, 5, 4, 3, 2]

Instead, you can realize the start value like this

>>> it[len(it)-3 : 0 : -1]
[7, 6, 5, 4, 3, 2]
thefourtheye
  • 233,700
  • 52
  • 457
  • 497
  • `[7, 6, 5, 4, 3, 2]` is what I'm looking for, not `[8, 7, 6, 5, 4, 3, 2]` — i.e., `it[1:-2][::-1]`. – orome Oct 26 '15 at 17:50
2

I think the other two answers disambiguate the usage of slicing and give a clearer image of how its parameters work.


But, since your question also involves readability -- which, let's not forget, is a big factor especially in Python -- I'd like to point out how you can improve it slightly by assigning slice() objects to variables thus removing all those hardcoded : separated numbers.

Your truncate and reverse slice object could, alternatively, be coded with a usage implying name :

rev_slice = slice(-3, 0, -1)

In some other config-like file. You could then use it in its named glory within slicing operations to make this a bit more easy on the eyes :

it[rev_slice]  # [7, 6, 5, 4, 3, 2] 

This might be a trivial thing to mention, but I think it's probably worth it.

Dimitris Fasarakis Hilliard
  • 150,925
  • 31
  • 268
  • 253
  • Any idea on how performance of `slice` with a stride compares with `list` + `reversed`? – orome Oct 26 '15 at 20:52
  • 1
    Hm, this has been adressed [here](http://stackoverflow.com/questions/3705670/best-way-to-create-a-reversed-list-in-python) so linking to this answer is the right thing to do (instead of just copying the contents). Additionally, as mentioned in the comments you'll have to make a tradeoff between readability and slight speed performance. If you have really large arrays, why don't you tackle this with good ol' numpy? – Dimitris Fasarakis Hilliard Oct 27 '15 at 05:06
2

Why not create a function for readability:

def listify(it, start=0, stop=None, rev=False):
    if stop is None:
        the_list = it[start:]
    else:
        the_list = it[start:stop]
    if rev:
        return the_list[::-1]
    else:
        return the_list

listify(it, start=1, stop=-2)  # [2, 3, 4, 5, 6, 7]
listify(it, start=1, stop=-2, rev=True)  # [7, 6, 5, 4, 3, 2]
Jim K
  • 12,824
  • 2
  • 22
  • 51
0

A good way to intuitively understand the Python slicing syntax is to see how it maps to the corresponding C for loop.

A slice like

x[a:b:c]

gives you the same elements as

for (int i = a; i < b; i += c) {
  ...
}

The special cases are just default values:

  • a defaults to 0
  • b defaults to len(x)
  • c defaults to 1

Plus one more special case:

  • if c is negative, then a and b are swapped and the < is inverted to a >
Daniel Pryden
  • 59,486
  • 16
  • 97
  • 135
  • Your statement that "`a` and `b` are swapped" in the negative step case seems very misleading. I think you mean that the default values are swapped, but that's not quite true. The default start value for a reversed slice is `-1` and the default end value is `-len(x)-1`. You can't specify that end index with a positive number, if you start at `len(x)-1` and try counting down, the end index will be `-1`, which means something different. – Blckknght Oct 27 '15 at 06:26
  • @Blckknght: Good point. My mental model usually only thinks about the positive case. I'm not sure how best to articulate the negative case here. – Daniel Pryden Oct 28 '15 at 01:26
  • Hmm, looking in [the cpython source for `list` slicing](https://hg.python.org/cpython/file/default/Objects/listobject.c#l2415), I see it doesn't directly use `stop` at all in the C `for` loop, but rather uses the computed number of items that will be in the slice and just counts that many items while incrementing an index variable by `step`. The [code for slice objects](https://hg.python.org/cpython/file/default/Objects/sliceobject.c#l246) that does the length calculation has `if (step < 0)` checks all over the place. – Blckknght Oct 28 '15 at 02:19
  • It might be more useful to think in Python, rather than `C`. Except where negative or (otherwise out of bounds) start or stop values are concerned, `x[start:stop:step]` is a lot like `[x[i] for i in range(start, stop, step)]`. You can get exactly correct handling of the edge cases by using a `slice` object and calling its `indicies` method with the length of our list to get the parameters for `range`: `[x[i] for i in range(*slice(start, stop, step).indicies(len(x)))]` – Blckknght Oct 28 '15 at 02:25