IT 117: Intermediate Scripting
Class 28 - Final Review
Review
Microphone
Final Exam
The final exam will be held on Thursday, May 16th from
3 to 6 PM.
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 know
- How to write a regular expression
- How to use the os and
sys modules
- How to write a class definition
- How to write a recursive function
A good way to study for the code questions is to review the Class Exercises
homework solutions and Class Notes.
Today's class will be a review session.
You will only be responsible for the material in today's class
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.
Course Evaluation
At the end of each semester we offer you the opportunity
to say what you think about this course.
What have I done right?
What have I done wrong?
What can I do better?
You are not asked for you name.
So the submissions are anonymous.
I will not see your responses until after I have submitted grades
for this course.
We collect this feedback through Course Evaluations.
I will use what you say to make this course better.
To complete the course evaluation, use the following
link
.
You have until Friday, May 24th, to submit the evaluation.
No Class Next Week
A normal semester has 28 classes.
But this semester has 29.
I do not have materials for a 29th class.
Next Tuesday I will have Zoom Office Hours, from 9 AM to 5 PM, instead of
a class.
The Zoom session will be conducted from my home in Somerville.
Not my office on the UMB campus.
Questions
Are there any questions before I begin?
Attendance
Review
Objects and Classes
- Objects
are chunks of RAM that can hold more than one value
- These values are called
attributes
- Objects contain functions that work on the attributes called
methods
- Objects do not have names
- Instead they have a location in RAM
- When you create an object you store this location in a variable
- You then use
dot notation
on this object to call its methods
- A
class
defines an object
- The class serves as a template from which objects are created
- An object created from a class is an
instance
of that class
- Some classes, like strings, are built into the Python interpreter
- But you can create your own classes
Classes Are Used by Other Programs
- Classes are usually contained in
modules
- In other words, in separate files
- To use a class you have to import it
- In this class we will create module files in the same directory as the
script
- The modules can be put in other directories
- But then we would have to change the
PYTHONPATH system variables
- The script that uses a class is often called the client program
Defining a Class
- The general format for a class is
class CLASS_NAME:
def __init__(self[, PARAMETER, ...]):
...
def METHOD_NAME(self[, PARAMETER, ...]):
...
- By convention, all class names in Python are Capitalized
- A class definition consists of a number of methods
- One of which has the special name __init__
- This method is a constructor, which is used to create the object
The Constructor Is Special
- A constructor is a special method that creates an object ...
- and returns a pointer to the object
- It has no
return
statement
- Every time you create a class you should have a constructor
Creating An Object from A Class
The self Variable
Data Hiding
The __str__ Method
- Classes can have certain methods whose names begin and end with
__
- These methods are sometime called
magic methods
- They are special because they are never called directly
- __init__ is an example of a magic method
- The client code calls __init__ using
the name of the class
t1 = Time("09:45:00")
- __str__ is another magic method
- It is called whenever you need a string representing an object
- Like when you print it
print(t1)
- If Time had no __str__
method and we tried to print it ...
- here is what we would get
>>> from time_3 import Time
>>> t = Time("09:30:00")
>>> print(t)
<time_3.Time object at 0x1013e8358>
- The __str__ method must return a string
- Like this
# returns a string formated as follows
# H[H] hours M[M] minutes S[S] seconds
def __str__(seconds):
hours = seconds // (60 * 60)
remainder = seconds % (60 * 60)
minutes = remainder // 60
seconds = remainder % 60
return str(hours) + " hours " + str(minutes)+ " minutes " + str(seconds) + " seconds
Accessor and Mutator Methods
- A well designed class hides its attributes
- But the client code sometimes needs to see those values
- So you need to create special methods called
accessors
- Since it is common practice for the name of an accessor to begin with
"get" ...
- another name for an accessor is a
getter
- Let's say you need to keep track of each computer in an office
- You would need to record the following information about each machine
- Manufacturer
- Model
- Serial number
- Processor
- RAM
- Hostname
- Disk size
- The first thing we need to do is create a constructor
def __init__(self, manufacturer, model, serial_number, processor, ram, hostname, disk_size):
self.__manufacturer = manufacturer
self.__model = model
self.__serial_number = serial_number
self.__processor = processor
self.__ram = ram
self.__hostname = hostname
self.__disk_size = disk_size
- All the attributes are hidden ...
- so we need to create accessor for each
def get_manufacturer(self):
return self.__manufacturer
def get_model(self):
return self.__model
def get_serial_number(self):
return self.__serial_number
def get_processor(self):
return self.__processor
def get_processor(self):
return self.__processor
def get_ram(self):
return self.__ram
def get_hostname(self):
return self.__hostname
def get_disk_size(self):
return self.__disk_size
- The values of the following attributes should never change
- __manufacturer
- __model
- __serial_number
- __processor
- That is one of the reasons we hide attributes
- But some attributes could change with time
- __ram
- __hostname
- __disk_size
- We need special methods to change these values
- Such methods are called
mutators
- Since the common practice is to start a mutator name with "set" ...
- mutators are sometimes called
setters
- Here are the mutators for the Computer class
def set_ram(self, ram):
self.__ram = ram
def set_hostname(self, hostname):
self.__hostname = hostname
def set_disk_size(self, disk_size):
self.__disk_size = disk_siz
Storing Objects for Future Use
- Most computer languages allow you to store an object on disk
- This process is called
serializing
- Python calls this process
pickling
- Every Python installation comes with a standard module named
pickle
- This module contains functions used to serialize an object
- First you must import the pickle module
import pickle
- To open a file for writing binary data we must use the
wb access mode
pickle_file = open(pickle_filename, "wb")
- To write an object to disk we use the dump function
pickle.dump(students, pickle_file)
- Loading an object from a data file is a similar process
- First we open the file for binary reading
pickle_file = open(pickle_filename, "rb")
- Then we read the previously stored object using the
load function
students = pickle.load(pickle_file)
Import Statements and Global Variables
Magic Methods
- We can do things with numbers that do not involve using a method
- We can write expressions using operators like
x += 7
y = x + 8
x > y
- When we create objects we create methods that act on them
- The Time class
has a difference method ...
- that gives the difference between two Time
objects
- So if t1 and t2
are Time objects I can get the time
between them like this
diff = t1.difference(t2)
- But wouldn't it be better to be able to write
diff = t1 - t2
- We can if we create a
magic method
for subtraction
- For every arithmetic
operator
- there is an equivalent magic method
Operator | Magic Method |
+ |
__add__(self, other) |
- |
__sub__(self, other) |
* |
__mul__(self, other) |
// |
__floordiv__(self, other) |
/ |
__div__(self, other) |
% |
__mod__(self, other) |
** |
__pow__(self, other) |
- To get the difference between two
Time objects using
- ...
- all we have to do is change the name of the
difference method ...
- to __sub__
# returns the difference in seconds between two times
def __sub__(self, other_time):
return self.__seconds - other_time.__seconds
- Now we can simply use the subtraction operator,
-
- To tell the difference in seconds between two
Time objects
>>> t1 = Time("10:45:10")
>>> t2 = Time("10:50:00")
>>> t2 - t1
290
Magic Methods and Boolean Operators
- Boolean operators
are used to compare two things
- We can use magic methods to implement the boolean operators ...
- for any class we create
- Here they are
Operator | Magic Method |
== |
__eq__(self, other) |
!= |
__ne__(self, other) |
< |
__lt__(self, other) |
> |
__gt__(self, other) |
<= |
__le__(self, other) |
>= |
__ge__(self, other) |
- Implementing these magic methods is straightforward
def __eq__(self, other):
return self.__seconds == other.__seconds
def __ne__(self, other):
return self.__seconds != other.__seconds
def __lt__(self, other):
return self.__seconds < other.__seconds
def __gt__(self, other):
return self.__seconds > other.__seconds
def __le__(self, other):
return self.__seconds <= other.__seconds
def __ge__(self,other):
return self.__seconds >= other.__seconds
- And they work as you would expect
>>> t1 = Time("10:45:10")
>>> t2 = Time("10:50:00")
>>> t3 = Time("10:50:00")
>>> t1 == t2
False
t2 == t3
True
>>> t1 < t2
True
>>> t1 > t2
False
>>> t1 <= t2
True
>>> t2 <= t3
True
>>> t1 >= t2
False
>>> t2 >= t3
True
Type Conversion Magic Methods
- You can change the data type of a value ...
- by using one of Python's conversion functions
- We can use these functions on an object variable of a class ...
- if we implement the appropriate magic method
Operator |
Magic Method |
int |
__int__(self) |
float |
__float__(self) |
bool |
__bool__(self) |
str |
__str__(self) |
- We have already implemented __str__
- How we implement each of the other magic methods is up to us
- Since the __seconds attribute already stores an
integer
- The simplest way to implement __int__ is
just to return that value
def __int__(self):
return self.__seconds
- To turn an integer into a float we multiply by 1.0
def __float__(self):
return self.__seconds * 1.0
- But what about __bool__?
- In Python, any integer that is not zero is
True
>>> bool(0)
False
>>> bool(5)
True
>>> bool(-5)
True
- So let's do the same for our __bool__ method
def __bool__(self):
return self.__seconds != 0
- Here is how they work
>>> t1 = Time("10:15:00")
>>> t2 = Time("00:00:00")
>>> str(t1)
'10:15:0 AM'
>>> bool(t1)
True
>>> bool(t2)
False
>>> int(t1)
36900
>>> int(t2)
0
>>> float(t1)
36900.0
- You will find the code for the latest version of the
Time class
here
Recursive Functions
- The body of a function consists of statements
- A function call is a statement
- So a function can call another function
- It can even call itself
- A function that calls itself is a
recursive function
Writing Recursive Functions
Calculating the Factorial of a Number
- To make sure that recursion comes to an end ...
- you must have two things
- Code that that makes the recursion stop
- Code that works toward this end condition
- The condition that causes the program to end is called the
base case
- The factorial of n is written as
n!
- It is defined as product of multiplying all number from 1 ...
- up to n
- The formula for factorial is
n! = (n-1)! * n
- The factorial of 1 has a different value
1! = 1
- This is the base case
- Here is a recursive function to calculate the factorial
def factorial(num):
if num == 1:
return 1
else:
return factorial(num -1) * num
- Calling this function I get
>>> print("5!:",factorial(5))
5!: 120
Replacing Recursion with a Loop
- Anything that can be done with recursion ...
- can also be done with a loop
- Recursive functions take more memory than loops
- Each time you make a recursive call ...
- the interpreter sets aside a bit of memory for that call ...
- which is only released when the function ends
- So why use recursion at all?
- Because it can save time when writing code
- It is often easier to create a recursive algorithm ...
- than one that uses a loop
- Memory is relatively cheap
- And good programmers are expensive
- So it's often best to go with recursion
Direct versus Indirect Recursion
- Recursion
is where a function calls itself
- The functions above call themselves directly
- This is called
direct recursion
- But you can also have
indirect recursion
- In indirect recursion a function calls another function ...
- and that other function calls the original function
- So if function A called function
B ...
- and function B called function
A ...
- that would be an example of indirect recursion
- The can be any number of functions involved in indirect recursion
- So function A can call function
B
- Which calls function C
- Which calls function D
- Which calls function A