A list or a tuple is an ordered collection of values. The values that make up a list or tuple are called its elements, or its items. We will use the term element or item to mean the same thing. Lists and tuples are similar to strings, which are ordered collections of characters, except that the elements of a list can be of any type. Lists, tuples, and strings — and other collections that maintain the order of their items — are called sequences.
We’ve already seen lists in previous chapters, so we’ll start by looking at tuples.
We can group together pairs of values by surrounding with parentheses. For example:
This is an example of a data structure — a mechanism for grouping and organizing data to make it easier to use.
The pair is an example of a tuple. Generalizing this, a tuple can be used to group any number of items into a single compound value. Syntactically, a tuple is a comma-separated sequence of values. Although it is not necessary, it is conventional to enclose tuples in parentheses:
Tuples are useful for representing what other languages often call records — some related information that belongs together, like your student record. There is no description of what each of these fields means, but we can guess. A tuple lets us “chunk” together related information and use it as a single thing.
Tuples support the same sequence operations as strings. The index operator selects an element from a tuple.
But if we try to use item assignment to modify one of the elements of the tuple, we get an error:
So like strings, tuples are immutable. Once Python has created a tuple in memory, it cannot be changed.
Of course, even if we can’t modify the elements of a tuple, we can always make the julia variable reference a new tuple holding different information. To construct the new tuple, it is convenient that we can slice parts of the old tuple and join up the bits to make the new tuple. So if julia has a new recent film, we could change her variable to reference a new tuple that used some information from the old one:
To create a tuple with a single element (but you’re probably not likely to do that too often), we have to include the final comma, because without the final comma, Python treats the (5) below as an integer in parentheses:
>>> tup = (5,) >>> type(tup) <class 'tuple'> >>> x = (5) >>> type(x) <class 'int'>
Python has a very powerful tuple assignment feature that allows a tuple of variables on the left of an assignment to be assigned values from a tuple on the right of the assignment.
This does the equivalent of seven assignment statements, all on one easy line. One requirement is that the number of variables on the left must match the number of elements in the tuple.
One way to think of tuple assignment is as tuple packing/unpacking. In tuple packing, the values are ‘packed’ together in a tuple. In tuple unpacking, the values in a tuple are ‘unpacked’ into individual variables.
Once in a while, it is useful to swap the values of two variables. With conventional assignment statements, we have to use a temporary variable. For example, to swap a and b:
Tuple assignment solves this problem neatly:
The left side is a tuple of variables; the right side is a tuple of values. Each value is assigned to its respective variable. All the expressions on the right side are evaluated before any of the assignments. This feature makes tuple assignment quite versatile.
Naturally, the number of variables on the left and the number of values on the right have to be the same — if you try to run the code below, you’ll get an error:
Functions can always only return a single value, but by making that value a tuple, we can effectively group together as many values as we like, and return them together. This is very useful — we often want to know some batsman’s highest and lowest score, or we want to find the mean and the standard deviation, or we want to know the year, the month, and the day, or if we’re doing some some ecological modeling we may want to know the number of rabbits and the number of wolves on an island at a given time.
For example, we could write a function that returns both the area and the circumference of a circle of radius r:
There are several ways to create a new list; the simplest is to enclose the elements in square brackets ([ and ]):
The first example is a list of four integers. The second is a list of three strings. The elements of a list don’t have to be the same type. The following list contains a string, a float, an integer, and (amazingly) another list:
A list within another list is said to be nested.
Finally, a list with no elements is called an empty list, and is denoted [].
We can do more complicated things too. For example, we can make a list of tuples, where one of the items in the tuple is itself a list:
Tuples items can themselves be other tuples. For example, we could improve the information about our movie stars to hold the full date of birth rather than just the year, and we could have a list of some of her movies and dates that they were made, and so on:
Notice in this case that the tuple has just five elements — but each of those in turn can be another tuple, a list, a string, or any other kind of Python value. This property is known as being heterogeneous, meaning that it can be composed of elements of different types.
The syntax for accessing the elements of a list is the same as the syntax for accessing the characters of a string or a tuple — the index operator: [] (not to be confused with an empty list). The expression inside the brackets specifies the index. Remember that the indices start at 0:
Any expression evaluating to an integer can be used as an index:
If you try to access or assign to an element that does not exist, you get a runtime error:
It is common to use a loop variable as a list index.
Each time through the loop, the variable i is used as an index into the list, printing the i‘th element. This pattern of computation is called a list traversal.
The above sample doesn’t need or use the index i for anything besides getting the items from the list, so this more direct version — where the for loop gets the items — might be preferred:
The function len returns the length of a list, which is equal to the number of its elements. If you are going to use an integer index to access the list, it is a good idea to use this value as the upper bound of a loop instead of a constant. That way, if the size of the list changes, you won’t have to go through the program changing all the loops; they will work correctly for any size list:
The last time the body of the loop is executed, i is len(horsemen) - 1, which is the index of the last element. (But the version without the index looks even better now!)
Although a list can contain another list, the nested list still counts as a single element in its parent list. The length of this list is 4:
in and not in are Boolean operators that test membership in a sequence. We used them previously with strings, but they also work with lists and other sequences:
The + operator concatenates lists:
Similarly, the * operator repeats a list a given number of times:
The first example repeats [0] four times. The second example repeats the list [1, 2, 3] three times.
The slice operations we saw previously with strings let us work with sublists:
Unlike strings and tuples, lists are mutable, which means we can change their elements. Using the index operator on the left side of an assignment, we can update one of the elements:
The bracket operator applied to a list can appear anywhere in an expression. When it appears on the left side of an assignment, it changes one of the elements in the list, so the first element of fruit has been changed from "banana" to "pear", and the last from "quince" to "orange". An assignment to an element of a list is called item assignment. Item assignment does not work for strings:
but it does for lists:
With the slice operator we can update a whole sublist at once:
We can also remove elements from a list by assigning an empty list to them:
And we can add elements to a list by squeezing them into an empty slice at the desired location:
Using slices to delete list elements can be error-prone. Python provides an alternative that is more readable. The del statement removes an element from a list:
As you might expect, del causes a runtime error if the index is out of range.
You can also use del with a slice to delete a sublist:
As usual, the sublist selected by slice contains all the elements up to, but not including, the second index.
After we execute these assignment statements
we know that a and b will refer to a string object with the letters "banana". But we don’t know yet whether they point to the same string object.
There are two possible ways the Python interpreter could arrange its memory:
In one case, a and b refer to two different objects that have the same value. In the second case, they refer to the same object.
We can test whether two names refer to the same object using the is operator:
This tells us that both a and b refer to the same object, and that it is the second of the two state snapshots that accurately describes the relationship.
Since strings are immutable, Python optimizes resources by making two names that refer to the same string value refer to the same object.
This is not the case with lists:
The state snapshot here looks like this:
a and b have the same value but do not refer to the same object.
Since variables refer to objects, if we assign one variable to another, both variables refer to the same object:
In this case, the state snapshot looks like this:
Because the same list has two different names, a and b, we say that it is aliased. Changes made with one alias affect the other:
Although this behavior can be useful, it is sometimes unexpected or undesirable. In general, it is safer to avoid aliasing when you are working with mutable objects (i.e. lists at this point in our textbook, but we’ll meet more mutable objects as we cover classes and objects, dictionaries and sets). Of course, for immutable objects (i.e. strings, tuples), there’s no problem — it is just not possible to change something and get a surprise when you access an alias name. That’s why Python is free to alias strings (and any other immutable kinds of data) when it sees an opportunity to economize.
If we want to modify a list and also keep a copy of the original, we need to be able to make a copy of the list itself, not just the reference. This process is sometimes called cloning, to avoid the ambiguity of the word copy.
The easiest way to clone a list is to use the slice operator:
Taking any slice of a creates a new list. In this case the slice happens to consist of the whole list. So now the relationship is like this:
Now we are free to make changes to b without worrying that we’ll inadvertently be changing a:
The for loop also works with lists, as we’ve already seen. The generalized syntax of a for loop is:
So, as we’ve seen
It almost reads like English: For (every) friend in (the list of) friends, print (the name of the) friend.
Any list expression can be used in a for loop:
The first example prints all the multiples of 3 between 0 and 19. The second example expresses enthusiasm for various fruits.
Since lists are mutable, we often want to traverse a list, changing each of its elements. The following squares all the numbers in the list xs:
Take a moment to think about range(len(xs)) until you understand how it works.
Sometimes we are interested in both the value of an item (we want to square that value) and its index (so that we can assign the new value to that position). This pattern is common enough that Python provides a nicer way to implement it: enumerate generates pairs of both (index, value) during the list traversal. Try this next example to see more clearly how enumerate works:
The dot operator can also be used to access built-in methods of list objects. We’ll start with the most useful method for adding something onto the end of an existing list:
append is a list method which adds the argument passed to it to the end of the list. We’ll use it heavily when we’re creating new lists. Continuing with this example, we show several other list methods:
Experiment and play with the list methods shown here, and read their documentation until you feel confident that you understand how they work.
Passing a list as an argument actually passes a reference to the list, not a copy or clone of the list. So parameter passing creates an alias for you: the caller has one variable referencing the list, and the called function has an alias (a new variable pointing to the same list), but there is only one underlying list object.
For example, the function below takes a list as an argument and multiplies each element in the list by 2:
In the function above, the parameter a_list and the variable things are aliases for the same object. So before any changes to the elements in the list, the state snapshot looks like this:
If a function modifies the items of a list parameter, the caller sees the change.
Functions which take lists as arguments and change them during execution are called modifiers and the changes they make are called side effects.
A pure function does not produce side effects. It communicates with the calling program only through parameters, which it does not modify, and a return value. Here is double_stuff written as a pure function:
Notice that this version of double_stuff does not change its arguments:
An early rule we saw for assignment said “first evaluate the right hand side, then assign the resulting value to the variable”. So it is quite safe to assign the function result to the same variable that was passed to the function:
Which style is better?
Anything that can be done with modifiers can also be done with pure functions. In fact, some programming languages only allow pure functions. There is some evidence that programs that use pure functions are faster to develop and less error-prone than programs that use modifiers. Nevertheless, modifiers are convenient at times, and in some cases, functional programs are less efficient.
In general, we recommend that you write pure functions whenever it is reasonable to do so and resort to modifiers only if there is a compelling advantage. This approach might be called a functional programming style.
The pure version of double_stuff above made use of an important pattern for your toolbox. Whenever you need to write a function that creates and returns a list, the pattern is usually:
initialize a result variable to be an empty list loop create a new element append it to result return the result
Two of the most useful methods on strings involve conversion to and from lists of substrings. The split method (which we’ve already seen) breaks a string into a list of words. By default, any number of whitespace characters is considered a word boundary:
An optional argument called a delimiter can be used to specify which string to use as the boundary marker between substrings. The following example uses the string ai as the delimiter:
Notice that the delimiter doesn’t appear in the result.
The inverse of the split method is join. You choose a desired separator string, (often called the glue) and join the list with the glue between each of the elements:
The list that you glue together (wds in this example) is not modified. Also, as these next examples show, you can use empty glue or multi-character strings as glue:
Python has a built-in type conversion function called list that tries to turn whatever you give it into a list.
A nested list is a list that appears as an element in another list. In this list, the element with index 3 is a nested list:
If we output the element at index 3, we get:
To extract an element from the nested list, we can proceed in two steps:
Or we can combine them:
Bracket operators evaluate from left to right, so this expression gets the 3’th element of nested and extracts the 1’th element from it.
Nested lists are often used to represent matrices. For example, the matrix:
might be represented as:
mx is a list with three elements, where each element is a row of the matrix. We can select an entire row from the matrix in the usual way:
Or we can extract a single element from the matrix using the double-index form:
The first index selects the row, and the second index selects the column. Although this way of representing matrices is common, it is not the only possibility. A small variation is to use a list of columns instead of a list of rows. Later we will see a more radical alternative using a dictionary.