CS 1110 - Introduction to Programming

Starting Out with Python
Chapter 7, Lists and Tuples

Objectives:

This lesson covers chapter 7, which teaches us a new word, and a new meaning for an old one. Objectives important to this lesson:

  1. Sequences and lists
  2. List slicing
  3. Finding objects in lists: the in operator
  4. List methods
  5. Copying lists
  6. Processing lists
  7. Two-dimensional lists
  8. Tuples
Concepts:
Chapter 7
Sequences and Lists

The text gives us two definitions that are used in this chapter.

  • Sequence - a sequence is an object that can hold other objects, usually things that belong together for some reason. A string, for instance, is a sequence of characters.
  • Lists  - a list is a sequence of objects that can be of any data type. Consecutive elements (items in the sequence) can be different data types. The contents of a list can be changed by a program, including adding and removing elements.

When a list is created, the elements are enclosed by one set of square brackets, and the series of elements is delimited by commas. That's a long way to say that they look like this:

five_integers = [1, 2, 3, 4, 5]

Since a list is allowed to contain multiple data types, we could have a list that includes a combination of integers, strings, floats, etc. Often, you will want a list that contains elements of the same data type. The text offers an example that seems more valuable for longer sequences.

numbers = list(range(100))

In this example, the range() function will generate 100 numbers, from 0 to 99. Yes, you could also do it as range(1, 101) to get 1 to 100. They will be constructed as an iterable, which looks like a set, and could be used as the value list of a for-in loop. This may be confusing, in that iterables are written the same way that lists are written. That set of numbers is handed to the list() function, which creates a list holding those numbers, which is then assigned as the value of the variable called numbers, which is now the name of a list. This would beat typing out all those numbers to create the list by hand.

The text explains that we can use the * symbol in an expression with a list on its left, and an integer on its right, to repeat the list the number of times represented by the integer. For example, assume we has a list created like this: list_of_five = [1, 2, 3, 4, 5] We might use the repetition operator (the *) like this:

new_list = list_of_five * 3

This would create a new list called new_list that would contain [1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5]. The new list would contain the original list three times. The text does not mention it immediately, but this method can be used to create a list you intend to use and fill with real values later. Filling it with dummy values makes sure that the list exists and is the size you intend it to be. I could create a list to hold twenty elements, full of 0s for now, like this:

master_list = [0] * 20

Since a list does in fact look exactly like an iteration in a for-in loop, it should not be a surprise that we can use the name of a list to serve as an iteration. If I build a for-in loop with the new_list created above, it could begin like this:

for coconut in new_list:
    print(coconut + '\n')

The loop you see above would run fifteen times, and it would print the current value of coconut and a newline character each time.

The text mentions in passing that Python does not have arrays, since lists will do what arrays do and more besides that. We will reserve judgment about that for a bit, but will point out that the elements in a list have something in common with the elements in an array. We can reference them using index notation. Each element in the new_list example can be accessed as new_list[index_number]. Reading the list from left to right, the index numbers would start at 0, and run through one less than the number of elements.

If we wanted to access the list items from right to left, there is a notation for that. The item at the end of the list can be accessed as list_name[-1]. The next one's index would be -2, and so on.

Remember that indexes from left to right start at 0. The text warns us not to use code like this:

beatles = ['John', "Paul', 'George', 'Stu', 'Pete', 'Ringo']
index = 0
while index < 7
    print(beatles[index])
    index += 1

This would raise an exception error, an IndexError exception, because it would try to print 7 times, index values 0 through 6, and there is no element whose index is 6. If you don't understand that, back up four paragraphs and try it again.

It may occur to you that it would be handy to know how long a list happens to be, since the first index is always 0 and the last one is always "number of elements minus 1". The text tells us to use the len() function for this purpose. Pass the name of a sequence to the len() function. You will get a count of the elements in that sequence as a return value.

To make the code example above run correctly, we might rewrite the conditional line like this:

while index < len(beatles)

We can also assign values to the elements of a list, using the list[index] notation as we would use a variable name.

The text tells us that lists are mutable, which means we can assign new values to the various indexed positions in the list. You should have had no reason to think otherwise, but it is nice to know this is so, because we will be told the opposite about tuples, later in the chapter.

Another way to make larger lists is to concatenate two or more lists. We use the + operator to do so, but we are not doing math.

list3 = list1 + list2

The line above means to add the contents of list2 to the end of the contents of list1, then assign the entire list to list3. If we had only meant to add the elements of list2 to the end of list1 we could have written list1 += list2. In this example, we are still concatenating, but we are changing list1 instead of creating a new list.

List Slicing

Slicing means to select a subset of the elements in a list, which will be described by a sequence of indexes (or indices, depending on your grammar preference). In the example in the text, we have a list of the days of the week, Sunday through Saturday, stored in a list named days. If we wanted a slice of those elements stored in a new list we will call week_days, we could write a line like this:

week_days = days[1:6]

This notation is not very logical, but it's how the thing works. It means that week_days will hold copies of the elements of days having index values of 1, 2, 3, 4, and 5, stopping at and not including index 6. The text goes on to offer more examples, but confuses the issue by using lists of numbers.

  • Just remember that the first number in a slice notation is the first index value that is used, the the second number in a slice is the stop value, which is not included in the slice.
  • If there is no stop value in a slice, such as days[1:], that would mean to start at index 1, and go to the end of the list.
  • If there is no start or stop value, that means to use the whole list, which means there was no point to taking the slice.
  • If there are three numbers in the slice notation, the third number is a step value. For example we could type even_days = days[1:6:2]. This would select days starting at index[1], counting by twos, and having 6 as the stop value.
  • As if that were not enough, you can use a negative number as the start value, which is reckoned as an offset from the end of the list, not the beginning of it.
  • There are other rules we will leave for another day. Sufficient unto the day is the evil thereof.

Finding Objects in Lists: the in operator

The text tells us that we can verify whether a values exists in a list with the structure value in list_name, which is a lot shorter than running a loop and checking all the values stored in a list. The first example is in example program 7-2. The program creates a list and populates it with four strings. The program then asks the user for a string to look for. The program assigns the user's string to a variable called search, and runs this code:

    if search in prod_nums:
        print(search, 'was found in the list.')
    else:
        print(search, 'was not found in the list.')

As the text explains, the phrase value in list_name will return a True if the value is found, and will return a False if the value is not found. You can use not in, instead of in, if you want to test for a value being missing from a list.

List Methods and Useful Built-in Functions

As you can see in the last example, Python has some features that relate to lists. Table 7-1 shows us six methods. I have modified the table below to include code examples, which I think will help more than the information the table in the text gave.


Method Example/Description
append(item) list_name.append(item)
Calls the append() method of a list to add a new item to the end of the list.
index(item) list_name.index(item)
Calls the index() method of a list, which will return the index of the first instance of a specified value. Note: this will raise an exception if the value is not found, so you may want to verify that the item is in the list first.
insert(index, item) list_name.insert(index, item)
Calls the insert() method of a list, which inserts the new item at the specified index. The existing item at that index is moved to the next higher index, and the same is done for each of the other items in the list that were to the right of the insertion position.
sort() list_name.sort()
Calls the sort() method of a list, which reorders the items in the list according to the rules for < and >. Note that this changes the actual list, not a copy in a variable. If you want to retain the original order, make a copy first.
remove(item) list_name.remove(item)
Calls the remove() method of a list, which finds and deletes the first instance of the specified value in the list. Items in the list that followed the removed item are reassigned new indexes, starting with the index of the removed item. Note: this will raise an exception if the value is not found, so you may want to verify that the item is in the list first.
reverse() list_name.reverse()
Calls the reverse() method of a list, which reverses the order of the elements in the list. Note that this changes the actual list, not a copy in a variable. If you want to retain the original order, make a copy first. (See below.)

 

The text discusses three more tools that are useful with lists:

  • del - The del keyword is used to delete a specific item in a list. It may look like this:
    del list_name[index]
  • min() - The min() function takes a list name as its argument, and returns the element with the lowest value. It may look like this:
    min(list_name)
  • max() - The max() function takes a list name as its argument, and returns the element with the highest value. It may look like this:
    max(list_name)

Copying Lists

Making a copy of a list is more difficult that you might think. Before we start, think about this: what is a list? When we are working with a list in a program, a list is a set of values stored in memory. Consider this example:

list1 = ['a', 'b', 'c']
list2 = list1

The text informs us that the code in the lines above will create and populate list1, then set the label list2 to point to the same list in memory. The new list name, list2, does not create a new list. Essentially, this code simply creates an alternate name for the same list.

The text then shows us two ways to make an actual copy of a list. The first way is to use a loop and a list method. It could look like this:

list1 = ['a', 'b', 'c']
list2 = []  # this creates an empty list
for item in list1
    list2.append(item) # this method appends each item from list1 to list2, and it avoids having to write to indexes that do not exist in the empty list

Another way would have been to measure the length of list1, create a new list with the same number of (dummy) elements, then loop through list1, copying the value of each indexed element to the corresponding element in list2. The second way the text shows us is shorter, but the result is the same:

list1 = ['a', 'b', 'c']
list2 = []  + list1 # this creates a new empty list, then appends the contents of list1 to the new list

In both methods above, a new empty list is created first. This is a necessary step if we want there to be two actual lists in memory when we are done.

Processing Lists

  1. In example program 7-7, baristapay.py, the text starts the program knowing that there will be six baristas who work for our company, so it sets a constant, NUM_EMPLOYEES = 6.
  2. It then creates a list called hours that holds 6 zeros.
  3. The program then runs a loop that asks for the hours of each employee, and writes that number as a float in the corresponding element in the list.
  4. The program asks the pay rate, given that the same rate is used for all employees. This stored in a variable.
  5. The program then runs a second loop through the list, calculating and printing the gross pay earned by each employee.

The text remarks on the usefulness of using a constant for the number of employees, since it makes it very easy to change that one line of the program to accommodate a change in that number.

The text shows us an example of a program that runs through a list of numbers and calculates their total. This is done again in the next program example, but this time the program calculates the average of the numbers (mean, actually), by using the len() function to count the number of elements in the list.

The text pauses to consider how you might pass a list to a function as an argument. Having asked the question, you may be disappointed that there is no trick to it. You simply pass the name of the list and receive it in the called function as you would pass and receive any variable. It works the same way when a list is created and populated in a called function. That function can return the name of the list, and it will be accessible by the calling function.

The text introduces a file object method called writelines(), which can be used to write a list to a file. In example program 7-13, writelines.py, the program creates a list, cities, that holds four strings. It then opens a file, cities.txt, in w mode. It then calls the writelines method of the file object, outfile, to write the list to the file:

outfile.writelines(cities)

The file is then closed, and that example is done. The problem with that method is that the data goes into the file as one string, without any clue to where the individual strings should break. The text presents an alternative method: use a loop that can use the file object write() method to write each element of the list, concatenated with a newline character, which will put each element on a line by itself. This works better for our purposes.

On page 322, we are introduced to the readlines() method of a file object. This method reads each line in a file, and returns a list of strings, each string being a single line. As we have seen before, we may or may not want the newline character that ends each string in the data. If we do not want it, there is an example in program 7-15 that uses the list's rstrip() method to strip off that trailing character from each string in the list. Program 7-16 does the reverse: it takes a list of numbers, turns them into strings, concatenates a newline on each string, and writes each string to a file. Program 7-17 shows how to read all the lines in a file into a list, then run a loop to read each element, convert it to an int, then overwrite the string version with the int version in the list. We can talk about these examples in class.

Two-Dimensional Lists

We have seen lists in this chapter that held numbers, and lists that held strings. The text explains that any object can be an element in a list, including other lists. A two-dimensional list is just a list whose elements are also lists. The text shows us that printing any element of such a list will print the entire list that is stored at that element. That's nice, but what if we only wanted to access some of the elements of one of those nested lists? The text introduces an idea that leads to a notation that will let us access what we please.

In the example in the text, we start with a list called students. It holds three elements, each of which is a list of two students. The text represents this as a table with two columns, the size of the nested lists, and three rows, the number of lists in the outer list.

data held in students list
element 0 in nested list
element 1 in nested list
element/nested list 0
students[0][0] = 'Joe'
students[0][1] = 'Kim'
element/nested list 1
students[1][0] = 'Sam'
students[1][1] = 'Sue'
element/nested list 2
students[2][0] = 'Kelly'
students[2][1] = 'Chris'

Thinking about it this way, it should be more obvious why this kind of list is called two-dimensional. Each row holds one of the nested lists. Each column holds the elements at a particular position in one of the lists. As I have noted in the table, you can reference each element by the name of the two-dimensional list, followed by two subscripts, the first one for the row, and the second one for the column. Think of those as being like x and y coordinates on a graph. Horizontal reference first, then vertical reference. The text shows us another example, one having three columns, and three rows. Note that this still uses two coordinates, one for the row and one for the column. In program 7-18, the text shows us how to fill such a bunch of lists with two loops, one inside the other. The inner loop fills a row, while the outer loop controls what row is being filled. We could ask the user for a value, or we could do as the text does, generating a random value in each case.

Tuples

The last topic we will cover in this chapter is tuples. The text explains that a tuple is like a list, but it is immutable. Think of it as a list that is also a constant. There is a notation difference as well. The elements of a list are enclosed in square brackets. The elements of a tuple are enclose in parentheses. Example:

my_list = [1, 2, 3]
my_tuple = (1, 2, 3)

The text demonstrates looping through a tuple (to read it, because you can;t modify it) using two notations:

for n in my_tuple:

for n in range(len(my_tuple)):

The text tells us that tuples will support most of the things we can do with lists, but tuples do not contain these methods:

  • append
  • remove
  • insert
  • reverse
  • sort

Showing some sensitivity to the reader, the text asks "what is the point of having tuples?" For some unspecified reason, you can process tuples faster to process than you can process lists. The author seems to like tuples because you can't change them, making them safer for testing. Maybe, but that still doesn't sound like a good reason to me.

  • Should you need to turn a tuple into a list, call the list() function with the name of the tuple as your argument. 
  • Should you need to turn a list into a tuple, call the tuple() function with the name of the list as your argument.


Assignments

Assignments for these chapters will be found in Blackboard. We will explore that in class.