## IT 116: Introduction to Scripting Class 25

### Homework 11

I have posted homework 11 here.

It is due this coming Sunday at 11:59 PM.

This is the last homework assignment.

### Final Exam

The final exam will be given on Thursday, December 21st from 11:30 - 2:30.

The exam will be given in Wheatley 1-034.

It will consist of questions like those on the quizzes along with questions asking you to write short segments of Python code.

60% of the points on this exam will consist of questions from the Ungraded Class Quizzes.

The last class, Tuesday, December 12th, will be a review session.

You will only be responsible for the material in the Class Notes for that class and the review for the Mid-term, which you will find here.

Although the time alloted for the exam is 3 hours, I would expect that most of you would not need that much time.

You will not be able to leave the room until you turn in your exam paper so you should visit the restroom before you take the test.

The final is a closed book exam.

### Review

#### Problems with Copying Objects

• If we wanted to make a copy of a variable holding a number ...
• we would use an assignment statement
```>>> number_1 = 5
>>> number_2 = number_1
>>> number_2
5```
• If you then change number_1 ...
• the value of number_2 remains unchanged
```>>> number_1 = 6
>>> number_1
6
>>> number_2
5```
• The same is not true for objects, such as lists
• When we write something like this
```>>> list_1 = [1,2,3,4,5,6,7]
>>> list_1
[1, 2, 3, 4, 5, 6, 7]```
• When you try to assign the value of list_1 to a new variable
• things at first look the same
```>>> list_2 = list_1
>>> list_2
[1, 2, 3, 4, 5, 6, 7]```
• But when you change something in list_1 ...
• the same change appears in list_2
```>>> list_1[6] = 8
>>> list_1
[1, 2, 3, 4, 5, 6, 8]
>>> list_2
[1, 2, 3, 4, 5, 6, 8]```
• When we first created list_1
• this is what we have in memory
• When we copy list_1 to list_2 ...
• this is what we have
• When we make a change to list_1 ...
• it makes the same change to list_2 ...
• because both variables refer to the same list

#### Copying Lists

• To truly copy a list ...
• we must make a new list
• This new list must have the same elements of the original list ...
• in the same order
• To make a new copy of the first list we use the + operator ...
• which concatenates two lists
• In other words it creates a new list ...
• which has all the elements of the first list ...
• followed by all the elements of the second list
• If we concatenate the empty list, [], with another list ...
• we get a copy of the other list
```>>> list_1
[1, 2, 3, 4, 5, 6, 7]
>>> list_2 = [] + list_1
>>> list_2
[1, 2, 3, 4, 5, 6, 7]```
• Here is the picture in memory
• Now when you change one list ...
• the other remains unchanged
```>>> list_1[6] = 8
>>> list_1
[1, 2, 3, 4, 5, 6, 8]
>>> list_2
[1, 2, 3, 4, 5, 6, 7]```
• In memory, it looks like this
• You can also copy a list using a slice
```>>> list_1 = [1,2,3,4,5,6,7]
>>> list_2 = list_1[0:len(list_1)]
>>> list_1[6] = 9
>>> list_1
[1, 2, 3, 4, 5, 6, 9]
>>>  list_2
[1, 2, 3, 4, 5, 6, 7]```
• There is a shorter way to do the same thing
• Remember that when you omit the first index in a slice ...
• Python assumes you mean 0
• And when you omit the second index ...
• Python assumes you mean the length of the list
• so we can write
```>>> list_1 = [1,2,3,4,5,6,7]
>>> list_2 = list_1[:]
>>> list_1[6] = 10
>>> list_1
[1, 2, 3, 4, 5, 6, 10]
>>> list_2
[1, 2, 3, 4, 5, 6, 7]```

#### Processing Lists

• In previous classes we have read in data from a file ...
• and then added all the numbers to get a total ...
• at the same time we were using a loop ...
• to read in the values
• But there are some things we cannot do using a single loop
• What if we had a file containing the temperature at noon ...
• and wanted to calculate the number of days ...
• the temperature was above average?
• We can't calculate this last value ...
• in the same loop that reads in the values ...
• because we don't know the average until the fist loop finished
• One way to solve this problem ...
• is to add another operation to the `while` loop
• In addition to these operations
• Counting the values
• We can add the operation of storing the values in a list
• That way we can perform other operations on the data
• Once the first loop calculates the average ...
• and loads the numbers into a list ...
• I can use a second loop to count the days above average
• I can loop through the list ...
• and count the number of days ...
• where the temperature is above average

#### Functions That Return Lists

• We are used to functions returning values ...
• but they can also return objects
• Here is a function that reads numbers from a file ...
• loads them into a new list ...
• and returns the list
```#! /usr/bin/python3
# reads a text file containing integers
# and prints it

# reads a text file of integers
# and stores them in a list which is returned
file = open(filename, "r")
new_list = []
for line in file:
number = int(line)
new_list.append(number)
file.close()
return new_list

print("List:", number_list)```

#### Functions That Return Objects

• I just said that the function read_integers_into_list returns a list
• That is not quite correct
• When a function runs it gets it's own memory space ...
• that is different from the memory space of the rest of the program
• This memory space is given to the function when it starts ...
• and disappears once the function finishes
• When the function creates the empty list
`new_list = []`
• The list object is created in a special place in memory reserved for objects
• The local variable new_list lives in the functions memory space ...
• that will go away when the function exits
• But the list itself lives in a different memory space ...
• that will not vanish when the function ends
• The last thing the function does before it exits ...
• is return the value of new_lsit ...
• to the statement that calls it
`number_list = read_integers_into_list("temperatures.txt")`
• Since the value of new_list is the location of the list object ...
• the main body of the script now has access to the list

#### Functions That Work with Lists

• Once you have a list of values ...
• you can use it as an argument to a function ...
• which can return a value calculated from that function ...
• like calculating the average
```def average_list(list):
total = 0
for index in range(len(list)):
total += list[index]
• The procedure is simple
• Set an accumulator to 0 ...
• then loop through the list ...
• adding each number to the accumulator
• When the loop ends ...
• return the accumulator divided by the length of the list

#### Functions That Change Lists

• If I have a function that takes a number as an argument ...
• performs some calculation ...
• it can give the result of that calculation back ...
• to the calling statement ...
• using a `return` statement
```def double_number(num):
...   return 2 * num
...
>>> result = double_number(5)
>>> result
10```
• But if we write a function that performs some operation ...
• on the elements of a list ...
• that function does not have to return anything ...
• yet it will change the list
```>>> def double_list(list):
...    for index in range(len(list)):
...       list[index] =  2 * list[index]
...
>>> numbers = [1,2,3,4,5]
>>> double_list(numbers)
>>> numbers
[2, 4, 6, 8, 10]```
• To understand why this works ...
• we have to remember that lists are objects
• The list itself lives in its own bit of memory
• In the function above the local variable list ...
• does not contain the list
• Instead it contains a reference to the list object
• When I then use numbers as an argument to double_list ...
• I am merely giving the function a reference to the object
• The function uses that reference to change the list
• The function does not have to return the local variable list ...
• because already has a reference to the list in the variable numbers
• You should think of object variables as "handles" for the object
• I pass the "handle" to the function ...
• and that "handle" allows the function to manipulate the object

### New Material

#### List Elements

• The elements of a list can be anything
```>>> list_1 = [1 , 2.5, True, "foo"]
>>> for index in range(len(list_1)):
...    print(list_1[index], type(list_1[index]))
...
1 <class 'int'>
2.5 <class 'float'>
True <class 'bool'>
foo <class 'str'>```
• The elements of a list can even be another list
```>>> list_2 = [ 1, 2, 3, 4, [5, 6, 7,]]
>>> for index in range(len(list_2)):
...    print(list_2[index], type(list_2[index]))
...
1 <class 'int'>
2 <class 'int'>
3 <class 'int'>
4 <class 'int'>
[5, 6, 7] <class 'list'>```

#### Two-Dimensional Lists

• When all the elements of a list are themselves list ...
• we have a two-dimensional list
```>>> two_d_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> two_d_list
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]```
• Each element of the list two_d_list ...
• is itself a list
• We can see this by looping through the elements
```>>> for index in range(len(two_d_list)):
...     print(two_d_list[index])
...
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]```
• Notice that we can access each element in this two-dimensional list ...
• the same way we access element in a one-dimensional list
• We first write the name of the list ...
• followed by and index number ...
• inside square brackets, [ ]
```>>> two_d_list[0]
[1, 2, 3]
>>>  two_d_list[1]
[4, 5, 6]
>>>  two_d_list[2]
[7, 8, 9]```
• If we wanted to access an element of one of the lists ...
• inside two_d_list ...
• we use the same technique
• But how do we get the names of these "inner" lists?
• The first inner list is just
`two_d_list[0]`
• To get the first element of this list we write
```>>> two_d_list[0][0]
1```
• The second element is
```>>> two_d_list[0][1]
2
```
• The third is
```>>> two_d_list[0][2]
3```
• We can think of this two-dimensional list as a square
```1 2 3
4 5 6
7 8 9```
• Just as each point on a two-dimensional graph ...
• with an X and a Y axis ...
• can be represented by two numbers ...
• each element in a two-dimensional list ...
• can be represented by two index numbers
• The first number determines the row ...
• and the second number determines the column
• So we can print the 2D list like this
```>>> for row in range(len(two_d_list)):
...     for column in range(len(two_d_list[row])):
...         print(two_d_list[row][column], end=" ')
...     print()
...
1 2 3
4 5 6
7 8 9 ```
• Maybe writing the loop another way makes it clearer ...
• what is going on
```for row in range(len(two_d_list)):
...     for column in range(len(two_d_list[row])):
...         print(two_d_list[row][column], end=" ')
...     print()
...
1 2 3
4 5 6
7 8 9 ```

#### Solving Problems with Two-Dimensional Lists

• Two-dimensional lists can be used to solve many problems
• For example, we can represent the following game of tic-tak-toe
• Using the following array
`game = [["O", "X", "X"], ["X", "0", "O"], ["X", "0", "O"]]`
• as we can see by printing the elements
```>>> for row in range(len(game)):
...     for column in range(len(game[row])):
...         print(game[row][column], end=" ")
...     print()
...
O X X
X 0 O
X 0 O ```
• As more practical example, consider a program to average student grades
• Let's say I have the following file of grades
```Sam  85 76 90 88
Jane 76 70 99 84
Sue  100 95 98 92```
• I'll read them in and create a two-dimensional list
• First I need a function to create the list
• The first element of each row will be the name of the student
• The second, third and fourth elements of each row are grades
• Here is a function that will do this
```def scores_to_list(filename):
file = open(filename, "r")
student_records = []
for line in file:
fields  = line.split()
for index in range(1, len(fields)):
fields[index] = int(fields[index])
student_records.append(fields)
return student_records```
• The first thing I do is create an empty list ...
• which will be the list I return ...
• to the calling statement
• Then I read in each line of the file ...
• and use split to turn the line ...
• into a list
• All the data from the line is in this list ...
• but the scores are text ...
• and I need to turn them into integers
• So I loop through the entries in the list ...
• skipping over the first entry which is the name ...
• and turn all the scores into integers
• This function returns the following list
`[['Sam', 85, 76, 90, 88], ['Jane', 76, 70, 99, 84], ['Sue', 100, 95, 98, 92]]`
• Next, I need a function that will print the contents of a list ...
• for each student
```def print_student_list(student_list):
for row in range(len(student_list)):
for column in range(len(student_list[row])):
print(student_list[row][column], end="\t")
print()```
• When I call this function I get
```Sam 85  76  90  88
Jane    76  70  99  84
Sue 100 95  98  92```
• Now I need a function to compute the average for each student
```def average(row_list):
total = 0
for column in range(1, len(row_list)):
total += row_list[column]
return round(total/(len(row_list) - 1))```
• This function is going to compute the average for each student
• Since the entries for a student are on a single row
• the parameter for this function is a row ...
• from the two-dimensional list of student scores
• The trick here is that the first entry in this row list ...
• is not a number, but the name of the student
• which we have to skip over when calculating the total
• When we calculate the average ...
• we need to divide by one less than the length of the row list
• Notice how I had to write the average calculation
`total/(len(row_list) - 1)`
• The first parentheses group
`len(row_list) - 1`
`total/len(row_list)\ - 1)`
• total would be divided by the length ...
• and then 1 would be subtracted from that result
• My last function will create a new two-dimensional list
• The first element will be the name of the student ...
• and the second will be their average score
```def average_list_create(list):
average_list = []
for row in list:       # loop through the rows of the 2D scores list
name = row[0]      # get name from scores list
ave = average(row) # compute the average for the row
average_list.append([name, ave]) # add a row to the new 2D list
return average_list```
• First we create a empty list ...
• for which we will create an row entry for each student ...
• Next we loop through the scores list
• We create an empty list that will be the row ...
• for our new two-dimensional list
• We add the students name to this list ...
• and the average ...
• which we calculate by a call to average
• Putting all of this together we have
```#! /usr/bin/python3
# computes the average

# reads in a list of names and grades and returns a two-dimensional list
# converting all scores to integers
def scores_to_list(filename):
file = open(filename, "r")
student_records = []
for line in file:
fields  = line.split()
for index in range(1, len(fields)):
fields[index] = int(fields[index])
student_records.append(fields)
return student_records

# prints the entries for each student in a two-dimensional list
def print_student_list(student_list):
for row in range(len(student_list)):
for column in range(len(student_list[row])):
print(student_list[row][column], end="\t")
print()

# returns the average for a student given the row of entries for a single student
def average(row_list):
total = 0
for column in range(1, len(row_list)):
total += row_list[column]
return round(total/(len(row_list) - 1))

# returns a list of students and their average score
def average_list_create(list):
average_list = []
for row in list:       # loop through the rows of the 2D scores list
name = row[0]      # get name from scores list
ave = average(row) # compute the average for the row
average_list.append([name, ave]) # add a row to the new 2D list
return average_list

print_student_list(scores_list)
print()
averages_list = average_list_create(scores_list)
print_student_list(averages_list)```
• When we run this script we get
```\$ ./scores_average.py
Sam 85  76  90  88
Jane    76  70  99  84
Sue 100 95  98  92

Sam 85
Jane    82
Sue 96```

#### Tuples

• A tuple is a sequence of values ...
• that cannot be changed
• We can create a tuple literal ...
• by enclosing a series of values inside parentheses
```>>> tuple_1 = (1, 2, 3, 4, 5)
>>> tuple_1
(1, 2, 3, 4, 5)```
• Just as with lists ...
• tuples can contain elements of any type
```>>> tuple_2 = (1, 2.5, False, "Sam")
>>> tuple_2
(1, 2.5, False, 'Sam')```
• You can access the elements of a tuple ...
• in the same way you do with a list
```>>> tuple_1[0]
1```
• You can use a `for` loop to print all the elements
```>>> for index in range(len(tuple_1)):
...     print(tuple_1[index], end=" ")
...
1 2 3 4 5```
• You can use the concatenation operator
```>>> tuple_3 = tuple_1 + tuple_2
>>> tuple_3
(1, 2, 3, 4, 5, 1, 2.5, False, 'Sam')```
• the repetition operator
```>>> tuple_4 = tuple_1 * 3
>>> tuple_4
(1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5)```
• and the `in` operator
```>>> "Sam" in tuple_2
True```
• The `len` function works with tuples
```>>> tuple_1
(1, 2, 3, 4, 5)
>>> len(tuple_1)
5```
• as does `min`
```>>> min(tuple_1)
1```
• `max`
```>>> max(tuple_1)
5```
• and the `index` method
```>>> tuple_1.index(3)
2```
• But you cannot use the following methods
• append
• remove
• insert
• sort
• reverse
...
• because they change a sequence ...
• and tuples cannot be changed
• Slices also work with tuples
```>>> tuple_1
(1, 2, 3, 4, 5)
>>> tuple_1[1:3]
(2, 3)
>>> tuple_1[:3]
(1, 2, 3)
>>> tuple_1[1:]
(2, 3, 4, 5)
>>> tuple_1[:]
(1, 2, 3, 4, 5)```
• One of the idiosyncrasies of tuples ...
• is how to create a tuple of one element
• If you try the obvious
`>>> tuple_5 = (1)`
• but when you print it, something strange happens
```>>> tuple_5
1```
• which is confirm when you use the `type` function
```type(tuple_5)
<class 'int'>```
• You must always use as comma, , ...
• when creating a tuple
• So here the correct way to do this
```>>> tuple_5 = (1,)
>>> tuple_5
(1,)
>>> type(tuple_5)
<class 'tuple'>
>>> tuple_5[0]
1```
• Python lets you create an empty tuple
```>>> empty_tuple = ()
>>> empty_tuple
()```
• It is a perfectly good tuple
```>>> type(empty_tuple)
<class 'tuple'>```

#### Converting between Lists and Tuples

• Python provides two functions that convert between lists and tuples
• `tuple` converts a list to a tuple
```>>> list_1 = ["a", "b", "c", "d", "e"]
>>> list_1
['a', 'b', 'c', 'd', 'e']
>>> tuple_2 = tuple(list_1)
>>> tuple_2
('a', 'b', 'c', 'd', 'e')
>>> type(tuple_2)
<class 'tuple'>```
• `list` converts a tuple into a list
```>>> list_1 = ["a", "b", "c", "d", "e"]
>>> list_1
['a', 'b', 'c', 'd', 'e']
>>> type(list_1)
<class 'list'>```
• Since lists and tuples are both sequences ...
• a function that works on one
• will work on the other
• as long as it doesn't use methods that don't work on sequences
```>>> def print_sequence(seq):
...     for index in range(len(seq)):
...         print(seq[index], end=" ")
...     print()
...
>>> print_sequence(list_1)
a b c d e
>>> print_sequence(tuple_1)
1 2 3 4 5```

#### Why Use Tuples?

• If lists and tuples are so similar ...
• but tuples have so many limitations ...
• why should we use tuples?
• There are two reasons
• Performance
• Security
• Because tuples are simpler that lists ...
• processing them is much faster ...
• than working with lists
• When dealing with large amounts of data ...
• the time savings you can by using tuples can be large
• Since tuples cannot be changed ...
• if you store data in a tuple ...
• you can be sure it will never be changed ...
• either accidentally or deliberately