\( \newcommand{\NOT}{\neg} \newcommand{\AND}{\wedge} \newcommand{\OR}{\vee} \newcommand{\XOR}{\oplus} \newcommand{\IMP}{\Rightarrow} \newcommand{\IFF}{\Leftrightarrow} \newcommand{\TRUE}{\text{True}\xspace} \newcommand{\FALSE}{\text{False}\xspace} \newcommand{\IN}{\,{\in}\,} \newcommand{\NOTIN}{\,{\notin}\,} \newcommand{\TO}{\rightarrow} \newcommand{\DIV}{\mid} \newcommand{\NDIV}{\nmid} \newcommand{\MOD}[1]{\pmod{#1}} \newcommand{\MODS}[1]{\ (\text{mod}\ #1)} \newcommand{\N}{\mathbb N} \newcommand{\Z}{\mathbb Z} \newcommand{\Q}{\mathbb Q} \newcommand{\R}{\mathbb R} \newcommand{\C}{\mathbb C} \newcommand{\cA}{\mathcal A} \newcommand{\cB}{\mathcal B} \newcommand{\cC}{\mathcal C} \newcommand{\cD}{\mathcal D} \newcommand{\cE}{\mathcal E} \newcommand{\cF}{\mathcal F} \newcommand{\cG}{\mathcal G} \newcommand{\cH}{\mathcal H} \newcommand{\cI}{\mathcal I} \newcommand{\cJ}{\mathcal J} \newcommand{\cL}{\mathcal L} \newcommand{\cK}{\mathcal K} \newcommand{\cN}{\mathcal N} \newcommand{\cO}{\mathcal O} \newcommand{\cP}{\mathcal P} \newcommand{\cQ}{\mathcal Q} \newcommand{\cS}{\mathcal S} \newcommand{\cT}{\mathcal T} \newcommand{\cV}{\mathcal V} \newcommand{\cW}{\mathcal W} \newcommand{\cZ}{\mathcal Z} \newcommand{\emp}{\emptyset} \newcommand{\bs}{\backslash} \newcommand{\floor}[1]{\left \lfloor #1 \right \rfloor} \newcommand{\ceil}[1]{\left \lceil #1 \right \rceil} \newcommand{\abs}[1]{\left | #1 \right |} \newcommand{\xspace}{} \newcommand{\proofheader}[1]{\underline{\textbf{#1}}} \)

12.1 Sequences Revisited: Ranges, Indexing, and Slicing

In this chapter, we’ll go over some Python features that we touched on in CSC110 but didn’t discuss as rigourously. We’ll be using most/all of these features in CSC111, so we’ve included this material to support your transition to that course!

range and the optional start

Recall that range is a special data type in Python that represents a sequence of numbers:

>>> [x for x in range(1, 5)]
[1, 2, 3, 4]

So far, we’ve called range with two arguments, a start and stop. It turns out that the start argument is optional and has a default value of 0 if not specified. That means that range(0, n) can be written equivalently as range(n): This form harmonizes nicely with the fact that the range’s stop argument is exclusive: because of this, range(n) has n elements, 0, 1, ..., n - 1.

>>> [x for x in range(5)]
[0, 1, 2, 3, 4]

Indexing with negative numbers

Given any sequence Python data type (e.g., str, list, tuple), we know that we can use indexing to access an element by index in the sequence:

>>> [10, 20, 30, 40][2]
30
>>> 'Hello'[4]
'o'

One common task is to access elements offset from the end of the sequence rather than the front. For this, we can use indexing with negative numbers: for a sequence seq and positive integer 0 < i < len(seq), seq[-i] is equivalent to seq[len(seq) - i]. Here are some examples: Our examples here only use the list and str data types, but you know two other sequence data types: tuple and range. We encourage you to try these examples with those data types in the Python console!

>>> seq = [10, 20, 30, 40]
>>> seq[-1]  # equivalent to seq[4 - 1] == seq[3]
40
>>> 'Hello'[-2]  # equivalent to 'Hello'[5 - 2] == 'Hello'[3]
'l'

Warning: negative indexing is subject to index errors, just like non-negative indexing! For example, the following raises an IndexError, since 10 > len('Hello'):

>>> 'Hello'[-10]
Traceback (most recent call last):
  ...
IndexError: string index out of range

Sequence slicing

Given a sequence seq, we call seq[i] a (sequence) indexing operation. We have an additional syntax that allows us to access a subsequence of the sequence by specifying a start and stop index for a range of elements to retrive. Formally, the following syntax is called a (sequence) slicing operation:

seq[i:j]

Here, i and j are integers interpreted as indexes into seq, and the expression seq[i:j] evaluates to a new sequence that:

Here are some examples:

>>> [10, 20, 30, 40][1:3]
[20, 30]
>>> 'Hello'[0:3]
'Hel'

Like the range data type, sequence slicing allows for an optional “start” index. If omitted, all elements from the start of the sequence to the stop index (exclusive) are returned:

>>> 'Hello'[:3]
'Hel'

But slicing goes a step further and allows you to omit the stop index as well. In this case, all elements from the start index to the end of the sequence are returned:

>>> 'Hello'[3:]
'lo'

And finally, it’s possible to omit both the start and stop indexes in a slicing operation. In this case, a copy of the sequence with all of its elements is returned:

>>> 'Hello'[:]
'Hello'

Specifying a step amount

So far, our range and sequence slicing operations have had an implicit “+1” when describing the range of numbers/indexes to include. It turns out that Python allows both range and slicing to take an optional “step” value to allow the user to specify other differences between consecutive numbers.

range(start, stop, step)

For range, we do so by passing three arguments, range(start, stop, step):

>>> [x for x in range(0, 10, 2)]
[0, 2, 4, 6, 8]
>>> [x for x in range(10, 0, -1)]
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

That second example is interesting! There, the step argument is -1, and so the sequence decreases by 1 at each number, starting at 10 and stopping at 0 (exclusive).

seq[start:stop:step]

We can apply the same idea to sequence slicing by including an additional colon and step number:

>>> message = 'David is cool'
>>> message[0:10:2]
'Dvdi '
>>> message[10:0:-1]
'oc si diva'

Moreover, with a negative step we can still omit the “start” and “stop” indexes in a slicing operation. The behaviour is analogous to our above examples, except now an omitted “start” is interpreted as “start at the end of the sequence” and an omitted “stop” is interpreted as “stop at the beginning of the sequence”.

>>> message[4::-1]
'divaD'
>>> message[:4:-1]
'looc si '
>>> message[::-1]
'looc si divaD'

The last example, message[::-1], is a common Python idiom for reversing a string and other types of sequences.