IT 116: Introduction to Scripting
Class 25
Review
New Material
Microphone
Graded Quiz 10
You can connect to Gradescope to take weekly graded quiz
today during the last 15 minutes of the class.
Once you start the quiz you have 15 minutes to finish it.
This will be the last quiz.
Final Exam
The final exam will be held on Thursday, May 16th from
11:30AM - 2:30PM.
The exam will be given in this room.
If for some reason you are not able to take the Final at the time it will
be offered, you MUST send an email to me before the exam
so we can make alternative arrangements.
The final 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.
You do not need to study a Class Quiz question if the topic is not
mentioned in either the Midterm or Final review.
The remaining 40% will come from 4 questions that ask you to write
some code.
To study for the code questions you should be able to
- Write a function to create a list from a file
- Write a function that changes a list
- Write a function that performs a calculation using the
elements of a list
- Write a function performs a calculation on the characters
in a string
A good way to study for the code questions is to review the Class Exercises
homework solutions and Class Notes.
The last class on Thursday, May 2nd, will be a review session.
You will only be responsible for the material in that review session,
which you will find here,
and the review for the Midterm, 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.
The final is a closed book exam.
To prevent cheating, certain
rules
will be enforced during the exam.
Remember, the Midterm and Final determine 50% of your grade.
Announcement
There will be no graded quiz next week.
Questions
Are there any questions before I begin?
Review
Time for Computer Operations
- Computers are very fast
- But different computer operations proceed at different speeds
- There are three speed regimes for accessing data
- Getting data from a user
- Getting data from a file
- Getting data from memory
Reading a File into a List
- Reading data from a file on disk is relatively slow
- It is much faster to read data from memory
- For this reason whenever you read a file in a script ...
- you should probably create a list ...
- from the data in a file
- Here an the algorithm for creating a list of integers ...
- from a file with one number on each line
create an empty list
for each line in the file:
convert the line into an integer
append the integer to the list
return the list
- Using this algorithm we can write the following function ...
- which takes a file object as its only parameter
-
def read_integers_into_list(file)
numbs = []
for line in file:
num = int(line)
numbs.append(num)
return numbs
Advantages of Using Lists with Files
- Reading a file into a list has a number of advantages
- We do not have to count the number of entries in the file
- We can get this value using
len
on the list object
- We can also use
min
and max
- We can sort the file values in ascending order using the list
sort method
- We can sort the values in descending order using
sort and reverse
Problems with Copying Objects
- When a variable holds a number
- You can create a new variable with an assignment statement
>>> n1 = 5
>>> n2 = n1
>>> n2
5
- If you then change n1
- The value of n2 remains unchanged
>>> n1 = 6
>>> n1
6
>>> n2
5
- This not true for objects ...
- such as lists
- Let's say we create a list object ...
- and store its location in the variable l1
>>> l1 = [1,2,3,4,5,6,7]
>>> l1
[1, 2, 3, 4, 5, 6, 7]
- You can create a new variable l2 ...
- by assigning it the value of l1
>>> l2 = l1
>>> l2
[1, 2, 3, 4, 5, 6, 7]
- But when you change something in l1 ...
- the same change appears in l2
>>> l1[6] = 8
>>> l1
[1, 2, 3, 4, 5, 6, 8]
>>> l2
[1, 2, 3, 4, 5, 6, 8]
- Let's look at what is happening in memory
- We first created l1
- Then we copied l1 to
l2
l2 = l1
- After this the picture in RAM looks like this
- l1 and l2
point to the same object
- When we make a change using l1
- It's the same as using l2 to make the change
>>> l1[6] = 8
- Using an assignment statement to create a new list variable ...
- does not create a new object
- It merely creates a new variable referring to the same list
Copying Lists
- To copy a list you need to create a new list object
- We can do this using
concatenation
and the
empty list
>>> l1
[1, 2, 3, 4, 5, 6, 7]
>>> l2 = [] + l1
>>> l2
[1, 2, 3, 4, 5, 6, 7]
- Here is the picture in memory
- Now when you change one list
- The other remains unchanged
>>> l1[6] = 8
>>> l1
[1, 2, 3, 4, 5, 6, 8]
>>> l2
[1, 2, 3, 4, 5, 6, 7]
- In memory, it looks like this
- You can also copy a list using a slice
>>> l1 = [1,2,3,4,5,6,7]
>>> l2 = l1[:]
>>> l1[6] = 10
>>> l1
[1, 2, 3, 4, 5, 6, 10]
>>> l2
[1, 2, 3, 4, 5, 6, 7]
Functions That Return Objects
- The function above returns a pointer to a list it creates
- But where is the list?
- When a function runs it gets its own chunk of memory
- All the function's variables live inside this memory
- The variables disappear when the function ends
- This brings up two questions
- Why doesn't the list disappear when the function ends?
- What's the point of returning the list variable?
- The list remains after the function ends ...
- because it was not created in the function's memory space
- Objects are created in the memory space for the script
- Not the memory space for the function
- Script memory space does not change when the function ends
- When a function returns a variable it returns the value of that
variable
- The value of new_list is the memory address of the
list
- And that memory address is outside the function's memory space
- The situation in RAM looks like this
Functions That Work with Lists
- Functions can work on the values contained in a list
- When we use such a function we give it a list variable as an argument
- The function uses the memory address of the list to access the list
- Here is an algorithm for a function that returns the average of a list of
numbers
set an accumulator to zero
loop through the list using the list address:
add each number to the accumulator
return the accumulator divided by the length of the list
- Here is the function
def average_list(list):
total = 0
for num in list:
total += num
return total/len(list)
Functions That Change Lists
- Lists are mutable
- Here is a function that doubles all the elements in a list of numbers
>>> 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]
- Notice that the function did not return a value
- It doesn't have to
- The calling function already has a variable pointing to the list
Attendance
New Material
List Elements
- The elements of a list can be anything
>>> l1 = [1 , 2.5, True, "foo"]
>>> for index in range(len(l1)):
... print(l1[index], type(l1[index]))
...
1 <class 'int'>
2.5 <class 'float'>
True <class 'bool'>
foo <class 'str'>
- The elements of a list can even be another list
>>> l2 = [ 1, 2, 3, 4, [5, 6, 7,]]
>>> for index in range(len(l2)):
... print(l2[index], type(l2[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 lists
- We have a
two-dimensional list
>>> 2dl = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> 2dl
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
- Each element of 2dl is also a list
>>> for element in 2dl:
... print(element)
...
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
- We can use indexing to get each sublist
>>> 2dl[0]
[1, 2, 3]
>>> 2dl[1]
[4, 5, 6]
>>> 2dl[2]
[7, 8, 9]
- How do we get each element within the sublist?
- With another [ ]
>>> 2dl[0][0]
1
>>> 2dl[0][1]
2
>>> 2dl[0][2]
3
- We can think of this two-dimensional list as a rectangle
1 2 3
4 5 6
7 8 9
- Here is a script that prints each number individually
>>> for row in range(len(2dl)):
... for column in range(len(2dl[row])):
... print(2dl[row][column], end=" ")
... print()
...
1 2 3
4 5 6
7 8 9
- I could rewrite this function without using indexes
>>> for row in 2dl:
... for column in row:
... print(column, end=" ")
... print()
- Notice that when I print a number I don't move down to the next line
- But when I finish a row I have to call
print
to start a new line
Tuples
- A
tuple
is a sequence of values that cannot be changed
- You create a tuple
literal
using parentheses, ( )
- They enclose a comma separated list of values
>>> t1 = (1, 2, 3, 4, 5)
>>> t1
(1, 2, 3, 4, 5)
- Tuples can contain elements of any type
>>> t2 = (1, 2.5, False, "Sam")
>>> t2
(1, 2.5, False, 'Sam')
- You can access the elements of a tuple with an index
>>> t1[0]
1
- You can use a
for
loop to print all the elements
>>> for number in t1:
... print(number)
...
1
2
3
4
5
- You can use the concatenation operator
>>> t3 = t1 + t2
>>> t3
(1, 2, 3, 4, 5, 1, 2.5, False, 'Sam')
- The repetition operator
>>> t4 = t1 * 3
>>> t4
(1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5)
- And the
in
operator
>>> "Sam" in t2
True
- The
len
function works with tuples
>>> t1
(1, 2, 3, 4, 5)
>>> len(t1)
5
- As do
min
and max
>>> min(t1)
1
>>> max(t1)
5
- And the
index
method
>>> t1.index(3)
2
- But you cannot use the following methods
- append
- pop
- insert
- sort
- reverse
- Because they change the tuple ...
- and tuples cannot be changed
- Slices also work with tuples
>>> t1
(1, 2, 3, 4, 5)
>>> t1[1:3]
(2, 3)
>>> t1[:3]
(1, 2, 3)
>>> t1[1:]
(2, 3, 4, 5)
>>> t1[:]
(1, 2, 3, 4, 5)
- But there is one unusual feature of tuples
- That feature is how you create a tuple literal with only one
entry
- If you try the obvious
>>> t5 = (1)
- Something strange happens
>>> t5
1
- If you run
type
on the variable you get
type(t5)
<class 'int'>
- The interpreter did not see
(1)
as a tuple
- It saw it as the integer 1 ...
- inside parentheses
- Parentheses in Python have two meanings
- They are used to group part of an expression to raise it's priority
>>> 4 + 3 * 5
19
>>> (4 + 3) * 5
35
- As well as being used in a tuple literal
- When in doubt, Python assumes parentheses are for grouping
- Which is why
(1)
is an integer
- To make it clear that you are defining a tuple with one element ...
- you must use as comma, , , after the
single element
>>> t5 = (1,)
>>> t5
(1,)
>>> type(t5)
<class 'tuple'>
>>> t5[0]
1
- Python lets you create an empty tuple like this
>>> empty_tuple = ()
>>> empty_tuple
()
- It is a perfectly good tuple
>>> type(empty_tuple)
<class 'tuple'>
The tuple
Conversion Function
- Every one of the basic data types in Python has a conversion function
- The conversion function for tuples is
tuple
- It will work on strings
>>> name = "Glenn"
>>> tuple(name)
('G', 'l', 'e', 'n', 'n')
- And lists
>>> digits = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
>>> tuple(digits)
(1, 2, 3, 4, 5, 6, 7, 8, 9, 0)
- But not on integers
>>> tuple(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
- Or booleans
>>> tuple(True)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'bool' object is not iterable
tuple
only works on
iterables
- An iterable is an object that can be used in a
for
loop
Why Use Tuples?
- Lists and tuples are similar
- But tuples have many limitations
- So why should we use tuples?
- There are two reasons
- Tuples are simpler that lists
- So a list takes more memory than a tuple of the same size
- The smaller size makes them somewhat faster to work with
- The savings can be significant when working with big data sets
- Consider the data collected by the European Gaia satellite
- It measured the position and movement of 1.7 billion stars
- You would not want to use lists for that data set ...
- Because the memory cost would be prohibitive
- Tuples also provide security
- They cannot be changed
- So any data you put in a tuple cannot be modified or corrupted ...
- by other functions
- This can be important in team projects ...
- where many people work with the same data
Tic-Tac-Toe
- Two-dimensional lists can be used for many things
- For example, we can use it to represent a game of tic-tak-toe
- Tic-Tac-Toe is a game for two players on a 3 by 3 square
- One player marks a cell in the square with an "X"
- The other with an "O"
- The goal is to get three in a row
- Either across, down or diagonally
- Like this
- We can represent the board above with a two-dimensional array
board = [["O", "X", "X"], ["X", "0", "O"], ["X", "0", "O"]]
- As you 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
- Let's write a Python script for the game
- A player will make moves against the computer
- Once again we will use
incremental development
- When writing this program
Tic-Tac-Toe - Step 1
- We need to define the board
- The board is a 3 by 3 square represented as a two-dimensional list
- In the beginning each square is empty
board = [[" ", " ", " "],[" ", " ", " "],[" ", " ", " "]]
- We need to be able to print the board ...
- and have to be careful when we do this
- A simple for loop won't work
>>> board = [[" ", " ", " "],[" ", " ", " "],[" ", " ", " "]]
>>> for row in board:
... print(row)
...
[' ', ' ', ' ']
[' ', ' ', ' ']
[' ', ' ', ' ']
- We need to see the lines between each entry ...
- and each row
- So an empty board should look like this
| |
-----
| |
-----
| |
- Printing a row is a discrete task
- So we should create, the helper function
print_row, to do this
- A row will have the contents of each square
- With a vertical bar, | , between them
- Here is the code
def print_row(row):
print(row[0] + "|" + row[1] + "|" + row[2])
- When I print the first row I get
| |
Tic-Tac-Toe - Step 2
Tic-Tac-Toe - Step 3
- Now we need a function that asks the user for a move
- It should prompt the user for two integers
- One for row and one for column
- The rows and columns will have numbers from 1 to 3
- But the function has to check that the entry is valid
- It has to check that
- There are two values
- Both values are integers
- Both values are between 1 and 3
- To validate the data we need a
while
loop
- We will use
input
to ask the user for two values ...
- and call
split
on their answer to create variables ...
- for row and col
- The loop will keep running until the user enters good values
- To check we have two values we use
len
...
- on the list returned by split
- We need to convert these values into integers ...
- inside a
try
/except
statement
- If the
except
clause fires ...
- we print and error message ...
- and try again
- To check that the values are between 1 and 3
- We create the following list
constant
GOOD_VALUES = [1,2,3]
- Then we check that each number is contained in
GOOD_VALUES
- The loop will keep running until the user entry is correct
- Then it will return the values
- Here is the function user_move
def user_move():
while True:
reply = input("Next move (row col): ")
fields = reply.split()
if len(fields) < 2:
print("Need a row number and a column number")
continue
row, col = fields
try:
row = int(row)
col = int(col)
except:
print(row, "or", col, "is not a number")
continue
if row not in GOOD_VALUES or col not in GOOD_VALUES:
print("Row and column values must be 1, 2, or 3")
continue
return row, col
- Notice the use of
continue
in the except
clause
- If converting a string into an integer causes a runtime error ...
- we want to print an error message ...
- and get a new pair of values from the user ...
- so there is no reason to continue with the rest of the loop code
continue
let's us do that
Tic-Tac-Toe - Step 4
- There is one thing that user_move does not test for
- It needs to be sure that the square is empty
- To do this we need a global variable
prev_moves = []
- Before returning the move, user_move
needs to check prev_moves
- If the move has been taken, it should print an error message
- And keep looping
- Otherwise it should add the move to prev_moves ...
- before returning the move
- Here is the new version of user_move
def user_move():
while True:
reply = input("Next move (row col): ")
fields = reply.split()
if len(fields) != 2:
print("Need a row number and a column number")
continue
row, col = fields
try:
row = int(row)
col = int(col)
except:
print(row, "or", col, "is not a number")
continue
if row not in GOOD_VALUES or col not in GOOD_VALUES:
print("Row and column values must be 1, 2, or 3")
continue
if (row, col) in prev_moves:
print(row, col, "is already taken")
continue
else:
prev_moves.append((row, col))
return row, col
- Notice that each entry in prev_moves ...
- is a tuple of two integers
- We never want entries in this list to change
- So we use tuples
Tic-Tac-Toe - Step 5
- Now we need to create the function
mark_square ...
- to record a move
- It is a very simple function
def mark_square(row, col, mark):
board[row - 1][col - 1] = mark
- Notice that I have to subtrack 1 from both row
and col
- That's because list uswe zero-based indexing ...
- but we want the user to use the values 1, 2 and 3
- To make things a little clearer, I define two constants
USER_MARK = "X"
MACHINE_MARK = "O"
- We need a
while
loop to run the game
- Eventually this loop will stop when the game is over
- For now, I just need a loop that will call user_move
- Then call mark_square
print_board()
print()
while True:
row, col = user_move()
mark_square(row, col, USER_MARK)
print_board()
print()
Tic-Tac-Toe - Step 6
- Now we need to write machine_move
- We can use randint to create the integers
- And we can specify that they must be between 1 and 3
- The only
data validation
we need is to check that the space is free
- The moves have to be created inside a
while
loop
def machine_move():
while True:
row = random.randint(1,3)
col = random.randint(1,3)
if (row, col) not in prev_moves:
prev_moves.append((row, col))
return row, col
- Now we can call machine_move inside the game loop
print_board()
print()
while True:
row, col = user_move()
mark_square(row, col, USER_MARK)
print_board()
print()
row, col = machine_move()
mark_square(row, col, MACHINE_MARK)
print_board()
print()
Tic-Tac-Toe - Step 7
- To jump out of the game loop and end play ...
- we need to know when someone has won
- We will create the function game_over to do this
- The code will be complicated
- We can make the problem simpler if we only check if a specific player has won
- Each time a player makes a move we call game_over
for that mark
- There are three ways a player can win
- The player's mark fills a row
- The player's mark fills a column
- The player's mark fills a diagonal
- But each possibility can be satisfied in more than one way
- We have to check each row for the correct set of marks
- There are three rows, so there are are three possible ways to win
- We have the same situation for each column
- But there are only two diagonals
- We are going to need a
for
loop to check the rows and columns
- And two separate
if
statements for the diagonals
- Each loop will have an inner loop
- Which checks each square in the row or column
- To make things cleaner, I will define the helper function
cell_match
- It returns
True
if the mark is found in a specific square
def cell_match(row, col, mark):
return board[row][col] == mark
- The function returns
True
if a mark is in the cell
- Each
for
loop checks three squares
- If one square does not match it returns False
- If it gets to the end of the inner loop ...
- it prints a win message ...
- and returns true
- Here is game_over
def game_over(mark):
# check rows
for row in [0,1,2]:
if cell_match(row, 0, mark) and cell_match(row, 1, mark) and cell_match(row, 2, mark):
print(mark , "wins!")
return True
# check columns
for col in [0,1,2]:
if cell_match(0, col, mark) and cell_match(1, col, mark) and cell_match(2, col, mark):
print(mark , "wins!")
return True
# check diagonals
if cell_match(0, 0, mark) and cell_match(1, 1, mark) and cell_match(2, 2, mark):
print(mark , "wins!")
return True
if cell_match(0, 2, mark) and cell_match(1, 1, mark) and cell_match(2, 0, mark):
print(mark , "wins!")
return True
return False
- Now I need to modify the game loop to call this function
- The call needs to happen after each player makes a move
print_board()
print()
while True:
row, col = user_move()
mark_square(row, col, USER_MARK)
print_board()
if game_over(USER_MARK):
break
print()
row, col = machine_move()
mark_square(row, col, MACHINE_MARK)
print_board()
if game_over(MACHINE_MARK):
break
print()
- You will find the complete script here
Privacy
Class Exercise
Class Quiz