Programming in PythonLists and Tuples
Let's revisit the spreadsheet example we discussed earlier: suppose you're writing a spreadsheet application and you want to introduce some functionality for highlighting every row whose third-column value is greater than 10:
20 | 16 | 2 | 1 | 19 |
9 | 12 | 15 | 1 | 19 |
7 | 2 | 1 | 15 | 4 |
19 | 6 | 16 | 4 | 7 |
3 | 14 | 3 | 1 | 1 |
16 | 5 | 15 | 6 | 6 |
14 | 9 | 7 | 18 | 15 |
15 | 9 | 3 | 9 | 16 |
13 | 6 | 13 | 10 | 20 |
10 | 14 | 5 | 8 | 8 |
4 | 13 | 16 | 15 | 9 |
16 | 9 | 4 | 14 | 1 |
17 | 9 | 4 | 3 | 8 |
2 | 6 | 4 | 6 | 14 |
15 | 8 | 14 | 3 | 14 |
14 | 19 | 8 | 17 | 10 |
18 | 8 | 9 | 5 | 9 |
4 | 4 | 5 | 5 | 8 |
11 | 8 | 1 | 14 | 2 |
12 | 11 | 13 | 19 | 7 |
We definitely don't want to think of 100 variable names for the 100 values in the table, and we don't want to write a line of code for each row. What we need is a way to store all of the rows (or columns) in an object designed to contain many objects. Python provides several such compound data structures, and in this section we will learn about two: lists and tuples.
Lists
A list
in Python is a compound data type for storing a finite ordered sequence of Python objects. Lists are mutable, meaning that they can be changed.
The simplest way to produce a list in a Python program is with a list literal, which requires listing the objects separated by commas and delimited by square brackets:
myList = [1, "flower", True, 7] x = 5 myOtherList = [1, x, x, 2] myOtherList
Exercise
What happens to myOtherList
in the example above if a different value is assigned to x
after myOtherList
is created?
Solution. The list doesn't change. The object associated with the variable x
is retrieved when the list is created, and after that point the list is no longer connected to the name x
.
Like strings, lists can be indexed to obtain their elements. Indexes in Python begin at 0:
myList = [1, "flower", True, 7] myList[0] # returns 1 myList[3] # returns 7
Negative indices can be used to count from the end:
myList = [1, "flower", True, 7] i = -2 myList[i]
If we set i
to the negative number myList[i]
would return "flower"
.
Sublists can be extracted by slicing. Indexing a list with [i:j]
returns the portion of the list from the
myList = [1, "flower", True, 7] myList[0:2]
Exercise
If i
= j
= myList[i:j]
is equal to ["flower", True]
.
The start or stop value of a slice can be omitted, in which case it defaults to the beginning or end of the list, respectively.
L = list(range(10,20)) # returns [10,11,12,...,19] L[2:] # returns [12,13,...,20] L[:4] # returns [10,11,12,13]
Slices can include a step value after a second colon. For example, L[1::2]
returns the elements of L
at positions 1, 3, 5, 7, and 9. The step value is often used with omitted start and stop values:
list(range(100, 200))[::2]
Exercise
What step value can be used to reverse a list?
[2,4,6,8][::k]
Solution. Going in reverse order through a list corresponds to stepping by each time. Setting k = -1
in the code block above, we see that [::-1]
does indeed reverse the list. Apparently the start and stop values for a list L
implicitly are implicitly set to -1
and -len(L)-1
when a negative step value is used.
Like strings, lists can be concatenated with the +
operator.
[1,2,3] + [4,5,6,7]
Exercise
Write a L
and a positive integer n
and rotates L
by n
positions. In other words, every element of the list should move forward n
positions, wrapping around to the beginning if it goes off the end of the list.
def rotate(L, n): "Cyclically shift the elements of L by n positions" # add code here def test_rotate(): assert rotate([1,2,3],1) == [3,1,2] assert rotate([1,2,3],2) == [2,3,1] assert rotate([1,2,3,4,5],8) == [3,4,5,1,2] return "Tests passed!" test_rotate()
Solution. We figure out where the list needs to be split and concatenate the two resulting sublists in the opposite order:
def rotate(L, n): "Cyclically shift the elements of L by n positions" k = len(L) - n % len(L) return L[k:] + L[:k]
Lists may be modified by combining indexing with assignment:
L = [4,-3,2] L[0] = 1 L[1:3] = [6,3] L
Exercise
Write a line of code which sets every even-indexed entry of a list L
to zero. Note that you can get a list of n
zeros with [0] * n
.
L = list(range(100))
Solution. L[::2] = [0] * (len(L)//2)
The list
class has 11 ordinary
L = [1,2,3] L.append(4) # add an element to the end L.clear() # remove all items from list L.copy() # return a copy of the list L.extend([5,6,7]) # add elements to the end L.index(6) # find index of list entry L.insert(3,"hey") # insert object before index L.pop(index=1) # remove object at given index L.remove("hey") # remove first occurrence of "hey" L.reverse() L.sort()
If you forget these methods, you can access them in an interactive session by running dir(list)
.
Note that each of these methods changes the list L
. They do not return a new list:
L = [1,2,3] return_val = L.reverse() print(type(return_val)) print(L)
Exercise
Explain the errors in the code below (there are two).
def remove_fives(L):
"Removes instances of 5 from a list"
return L.remove("5")
print(remove_fives(["1", "5", "5", "10"]))
Solution. The remove
method only removes one instances of "5"
(the first one). Also, this method modifies the argument supplied to the function; it does not return new list with the "5"
removed.
List comprehensions
Two of the most common ways of generating one list from another are (1) applying a given function to every element of the original list, and (2) retaining only those elements of the original list which satisfy a given criterion. These two operations are called map and filter, respectively.
def square(x):
return x*x
list(map(square, range(5))) # returns [0, 1, 4, 9, 16]
def iseven(x):
return x % 2 == 0
list(filter(iseven, range(5))) # returns [0,2,4]
The extra calls to list
in the examples above are required to see the result because map
and filter
are lazy: they return objects which promise to perform the specified calculation when it's needed.
Python provides a convenient
[x**2 for x in range(5) if x % 2 == 0]
Let's break this example down step-by-step: the first value of range(5)
is assigned to the variable x
, and then the if
expression is evaluated. If it's true, the expression x**2
is evaluated and stored as the first value of the list that is to be returned. Then the second value of range(5)
is assigned to x
, the condition is evaluated, and so on.
Exercise
Write a list comprehension which returns a list whose kth entry is the last digit of the kth three-digit prime number.
from sympy import isprime
Solution. Here's an example solution:
from sympy import isprime [str(k)[-1] for k in range(100,1000) if isprime(k)]
Exercise
Write a list comprehension which takes a list of lists and returns only those lists whose second element has a least five elements.
records = [[3, "flower", -1], [2, "rise", 3], [0, "basket", 0]]
Solution. Here's one solution:
[record for record in records if len(record[1]) >= 5]
Tuples
Tuples are very similar to lists, except that tuples are
row = (22,2.0,"tomato") row[2] # returns "tomato" row[2] = "squash" # throws TypeError
Programmers tend to use tuples instead of lists in situations where position in the tuple carries more meaning than order. For example, perhaps the tuple assigned to row
above describes a row of plants in a garden, with the three numbers indicating the number of plants, the number of weeks since they were planted, and the type of plant. We could have chosen some other order for those three values, as long as we're consistent about which position corresponds to which value. By contrast, the 22 heights of the plants on that row would typically be stored in a list, since the list order corresponds to something meaningful in that case (namely, the order of the plants in the row).
Functions often return multiple values by returning a tuple containing those values. You can access individual elements of a tuple without having to index the tuple using tuple unpacking:
mycolor = (1.0,1.0,0.44) r, g, b = mycolor b
The convention in Python for values you don't want to store is to assign them to the variable whose name is just an underscore. That way you don't have to think of names for those variables, and you signal to anyone reading your code that you are not using those values.
Tuple unpacking can be combined with list comprehension syntax. If we want to extract the first element from each tuple in a list of triples, for example, we can do that as follows:
L = [(1,2,3),(4,5,6),(7,8,9)] [a for (a,_,_) in L]
The value 1 is assigned to a
, the value 2 is assigned to the underscore variable, and then the value 3 is also assigned to the underscore variable (this overwrite is no problem since we aren't using that value anyway). Then a
is evaluated as the first element in the new list, and the process repeats for the remaining triples in the list.
Exercise
Write a list comprehension which adds the first two elements of each tuple in L
. (So for the example above, the resulting list should be [3, 9, 15]
.)
Solution. Same idea:
L = [(1,2,3),(4,5,6),(7,8,9)] [a+b for (a,b,_) in L]
Exercise
The fractional part of a positive real number is the part after the decimal point: it's defined to be the positive difference between and the greatest integer which is less than or equal to . You can find the fractional part of x
in Python with the expression x - int(x)
.
Find the fractional parts of the first 100 positive integer multiples of . Use the function extrema
(defined below) on the resulting array to find its least and greatest values. Find the ratio of the greatest value to the least.
from numpy import pi def extrema(L): "Return (min,max) of L" m = L[0] M = L[0] for element in L: if element > M: M = element elif element < m: m = element return (m,M)
Solution. We use tuple unpacking to extract the min and max values from the tuple returned by the extrema
function.
m,M = extrema([pi*k-int(pi*k) for k in range(1,101)]) M/m
The result is about 56.08.
A common pattern for generating new arrays combines list comprehension, tuple unpacking, and the function zip
. The zip
function takes two arrays and returns a single array of pairs of corresponding entries (or three arrays, in which case it returns an array of triples, etc.). For example,
zip(["a", "b", "c"], [1, 2, 3])
returns an object which is equivalent to [("a", 1), ("b", 2), ("c", 3)]
.
If we have three vectors , , and of equal length, then the vector sum can be computed using the expression [a + b + c for (a,b,c) in zip(A,B,C)]
.
Exercise
Suppose that is a list which stores the heights of 100 cylinders and is a list which stores their radii (in the same order). Write a
H = [1, 2, 3] R = [0.8, 1.0, 1.2]
Solution. We zip H
and R
and use the volume formula :
from numpy import pi H = [1, 2, 3] R = [0.8, 1.0, 1.2] [pi*r*r*h for (h,r) in zip(H,R)]
Exercises
Exercise
(Try doing this one without executing any code.) What will the value of L
be after the following block is executed?
L = [4, 8, 2]
L.append(7)
L.extend([3,-1,8])
L.insert(2, 1)
L.remove(8)
L = tuple(L)
Exercise
Write a function which takes a matrix M
and an index i
and returns the $i$th column of M
. Assume that M
is represented as a list of lists, where each list represents a row.
def select_col(M, i): pass # add code here def test_select_col(): assert select_col([[1,2],[3,4]],1) == [2,4] assert select_col([[7,8],[8,-2],[3,4]],1) == [8,-2,4] return "Tests passed!" test_select_col()
Solution. We use a list comprehension to select the appropriate entry from each row.
def select_col(M, i): return [row[i] for row in M] def test_select_col(): assert select_col([[1,2],[3,4]],1) == [2,4] assert select_col([[7,8],[8,-2],[3,4]],1) == [8,-2,4] return "Test passed!" test_select_col()
Exercise
Write a function which reverses the words in a sentence. For simplicity, you may assume that the sentence does not contain punctuation.
Hint: The string methods join
and split
might be helpful. You can see the documentation for these methods with help(str.join)
and help(str.split)
.
def reverse_words(sentence): pass # add code here def test_reverse_words(): assert reverse_words("The quick brown fox") == "fox brown quick The" assert reverse_words("") == "" return "Tests passed!" test_reverse_words()
Solution. We use the string method split
, which splits a string on a given character. This gives us a list of the words in the sentence, which we can reverse by indexing with a negative step and rejoin with the join
method.
def reverse_words(sentence): return " ".join(sentence.split(" ")[::-1])