Quantcast
Channel: Planet Python
Viewing all articles
Browse latest Browse all 22462

Thomas Guest: Negative Sequence Indices in Python

$
0
0

Supply a negative index when accessing a sequence and Python counts back from the end. So, for example, my_list[-2] is the penultimate element of my_list, which is much better than my_list[len(my_list)-2] or even *(++my_list.rbegin()).

That final example uses one of C++’s reverse iterators. It gets the penultimate element of a collection by advancing an iterator from the reversed beginning of that collection. If you’re addicted to negative indices you can use them with C++ arrays, sort of.

Negative array indices in C++
#include <iostream>

int main()
{
    char const * domain = "wordaligned.org";
    char const * end = domain + strlen(domain);
    std::cout << end[-3] << end[-2] << end[-1] << '\n';
    return 0;
}

Compiling and running this program outputs the string “org”.

Going back to Python, the valid indices into a sequence of length L are -L, -L+1, … , 0, 1, … L-1. Whenever you write code to calculate an index used for accessing a sequence, and especially if you’re catching any resulting IndexErrors, it’s worth checking if the result of the calculation can be negative, and if — in this case — you really do want the from-the-back indexing behaviour.

 
 
 
 
0
1
2
3
4
5
6
7
8
9
10
11
12
13
 
 
 
 
W
O
R
D
A
L
I
G
N
E
D
 
 
 
 
-14
-13
-12
-11
-10
-9
-8
-7
-6
-5
-4
-3
-2
-1
 
 
 
 

The power of negative indices increases with slicing. Take a slice of a sequence by supplying begin and end indices.

>>> domain = 'wordaligned.org'
>>> domain[4:9]
'align'
>>> domain[4:-4]
'aligned'
>>> digits = list(range(10))
>>> digits
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> digits[3:4]
[3]
>>> digits[1:-1]
[1, 2, 3, 4, 5, 6, 7, 8]

Omitting an index defaults it to the end of the sequence. Omit both indices and both ends of the sequence are defaulted, giving a sliced copy.

>>> domain[-3:]
'org'
>>> domain[:4]
'word'
>>> digits[:]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

I prefer the list(digits) form for copying digits but you should certainly be familiar with the digits[:] version.

You can supply any indices as slice limits, even ones which wouldn’t be valid for item access. Imagine laying your sequence out on an indexed chopping board, slicing it at the specified points, then taking whatever lies between these points.

>>> digits[-1000000]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range
>>> digits[1000000]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range
>>> digits[-1000000:1000000]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Sequence slicing also takes a step parameter.

>>> digits[::2]
[0, 2, 4, 6, 8]
>>> digits[1::2]
[1, 3, 5, 7, 9]

This parameter too can be negative. The sign of the step affects which limiting values the begin and end slice parameters default to. It’s subtle behaviour, but you soon get used to it.

>>> digits[0:10:-2]
[]
>>> digits[::-2]
[9, 7, 5, 3, 1]
>>> digits[-2::-2]
[8, 6, 4, 2, 0]

How do you reverse a string? Slice it back to front!

>>> domain[::-1]
'gro.dengiladrow'

To recap: the default slice limits are the start and end of the sequence, 0 and -1, or -1 and 0 if the step is negative. The default step is 1 whichever way round the limits are. When slicing, s[i:j:k], i and j may take any integer value, and k can take any integer value except0.

The zero value creates another interesting edge case. Here’s a function to return the last n items of a sequence.

>>> def tail(xs, n)
...     return xs[-n:]
...

It fails when n is 0.

>>> tail(digits, 3)
[7, 8, 9]
>>> tail(digits, 2)
[8, 9]
>>> tail(digits, 1)
[9]
>>> tail(digits, 0)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

By the way, we’ve already seen slicing working well with lists and strings. It also works nicely with range objects.

>>> r = range(10)
>>> r[::2]
range(0, 10, 2)
>>> r[1::2]
range(1, 10, 2)


Viewing all articles
Browse latest Browse all 22462

Trending Articles