IT 117: Intermediate Scripting
Class 24
Review
New Material
Microphone
Quiz 9
Let's look at the answers to
Quiz 9.
Readings
If you have the textbook read Chapter 12, Recursion,
sections 12.1 Introduction to Recursion,
12.2 Problem Solving with Recursion and
12.3 Examples of Recursive Algorithms.
Solution to Homework 10
I have posted a solution to homework 10
here.
Let's take a look.
Homework 11
I have posted homework 11
here.
This is the last homework assignment.
It is due this Sunday at 11:59 PM.
Announcement
The UMB IT Club and Boston Linux User Group
will hold a Linux InstallFest on Saturday, May 3rd, from
9 to 5 in the McCormack Conference Room, M03-0721.
If you have a machine on which you would like to install Linux
and would like some help in doing this,
bring it the InstallFest.
Volunteers from the Boston Linux User Group with be on hand
to help with the installation.
They will also help you install the Window Subsystem for Linux
(WSL) on your machine or install Linux as a dual boot.
You can also bring your questions about Linux or Unix to
the InstallFest.
The Boston Linux and Unix User Group counts among its members
some of the most knowledgeable Linux and Unix people in the
Boston area.
You will find directions to M03-0721
here
Questions
Are there any questions before I begin?
Review
The Movie Subclass
- Every movie entry will have the four attributes of the
Video class
- Collection number
- Name
- Length
- Format
- But it will also have the following additional attributes
- The constructor will set the values for __director
and __studio
- __actors will be a set ...
- to which entries will be added by a
mutator method
Creating a Subclass
Creating a Constructor for a Subclass
- We need to give the Movie constructor 5 arguments
- name
- length
- format
- director
- studio
- But the __init__ won't use the first three
values directly
- Instead it will call __init__ in the class
Video ...
- to set the first three attributes
- We refer to __init__ in the
Video class ...
- using dot notation and the class name
Video.__init__(...)
- Then we set the values of the attributes ...
- that are unique to the Movie class
- The actors will be stored in a set
- Their names will be added later
- All we need to do at this point is create an empty set
- Here is the code for the Movie constructor
def __init__(self, name, length, format, director, studio):
Video.__init__(self, name, length, format)
self.__director = director
self.__studio = studio
self.__actors = set()
- The Movie is a subclass of
Video
- So it inherits the __str__ method from
Video
- Along with the
accessors
>>> m1.get_collection_no()
1
>>> m1.get_name()
'Forbidden Planet'
>>> m1.get_length()
98
>>> m1.get_format()
'DVD'
Movie Accessor Methods
The add_actor Method
- add_actor is a mutator
- Used to add the names of actors
def add_actor(self, name):
self.__actors.add(name)
-
>>> m1.add_actor("Walter Pidgeon")
>>> m1.add_actor("Anne Francis")
>>> m1.add_actor("Leslie Nielson")
>>> m1.add_actor("Warren Stevens")
>>> m1.get_actors()
['Walter Pidgeon', 'Anne Francis', 'Leslie Nielson', 'Warren Stevens']
A __str__ Method for Movie
- Movie has more attributes
than Video
- And its __str__ method should show them
- We could try to create one like this
def __str__(self):
return str(self.__collection_no) + ": " + self.__name + ", " + \
str(self.__length) + " minutes, " + self.__format + \
" Directed by " + self.__director + ", " + self.__studio
- But that won't work
>>> str(m1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/glenn/workspace-mars/it117/resources_it117/code_it117/example_code_it117/11_chapter_example_code/movie.py", line 26, in __str__
' Directed by ' + self.__director + ', ' + self.__studio
AttributeError: 'Movie' object has no attribute '_Movie__collection_no'
- The first four attributes are contained in a
Video object
- Not in a Movie object
- We will call the __str__ method
in Video ...
- and then add the Movie attributes
- But we need a special function to call methods in the parent class
The super
Function
Creating Derived Objects
- Whenever we create an object of a derived class ...
- we are actually creating two objects ...
- that behave as one
- An object of the
derived class
- And an object of the
base class
- So when we create a Movie object ...
- we are also creating a Video object
- The picture in memory looks like this
- The object variable m1 points to the derived object
- And
super
points to the base class object
The Instructional Class
- Here is the list of attributes for the Instructional
class
- __course_name
- __company
- __disc_number
- __instructor
- __lectures
- The first 4 values will be set by the constructor
- The names of the lectures on the disc ...
- will be stored in a the list __lectures
The Instructional Constructor
- The constructor for
Instructional
will not take a name parameter
- I'll explain the reason for this in the next section
- But it has to call __init__
in Video ...
- which requires a name value
- So I will use the nonsense string "XXX" when calling that method
- Here is the code
def __init__(self, course_name, company, disc_number, instructor, length, format):
Video.__init__(self, "XXX", length, format)
self.__course_name = course_name
self.__company = company
self.__disc_number = disc_number
self.__instructor = instructor
self.__lectures = []
The get_name Method for Instructional
- In the Video and Movie classes
the name attribute holds the name of the video
- This value is set in the Video constructor
- But I want the name for an Instructional object to be different
- I want it to have the following format
COURSE_NAME: Disc DISC_NO
- In other words, I want the name value to be calculated from other values ...
- not stored in the name attribute
- This means I cannot use the version of get_name
inherited from the Video class
- This class needs its own version of get_name
def get_name(self):
return self.__course_name + ': Disc ' + str(self.__disc_number)
- Which works when I test it
>>> i1.get_name()
'Understanding the Universe: Disc 1'
The __str__ Method for Instructional
- In the Movie class __str__
called the __str__ method of Video
...
- and then appended the Movie attributes
- But if we call
super().__str__()
as we did in Movie
- We will get "XXX" in the output
- This is not what I want
- I want the string representation of my Instructional
videos to have a specific format
COLLECTION_NO: COURSE_NAME: Disc DISC_NO, INSTRUCTOR
- This means the only information I need from the
Video superclass ...
- is the collection_no
- So __str__ will call get_collection_no
from the Video class ...
- along with it's own version of get_name ...
- as well as value of __instructor
- Here is the code
def __str__(self):
return str(super().get_collection_no()) + ': ' +self.get_name() + ', ' + self.__instructor
- Notice that I did not use all of the attributes from the superclass
- __str__ doesn't need to show all the
attributes
- It just needs to create a reasonable string representing the object
- Here is the test
>>> from instructional import Instructional
>>> i1 = Instructional("Understanding the Universe", "Great Courses", 1, "Alex Filippenko", 180, "DVD")
>>> str(i1)
'1: Understanding the Universe: Disc 1, Alex Filippenko'
Polymorphism
- The methods we declare in a superclass ...
- will be available in all subclasses
- When a subclass has a method with the same name as the superclass ...
- the interpreter will use the subclass method
- The subclass method is said to override the superclass method
- This means that we can call any of the methods in the superclass ...
- on any instance of the subclass ...
- even though the definition of the method can be different in each subclass
- So we can create a list of subclass objects ...
- calling the same method on each one ...
- but getting very different results
>>> from movie import Movie
>>> m1 = Movie("Forbidden Planet", 98, "DVD", "Fred McLeod Wilcox", "MGM")
>>> from instructional import Instructional
>>> i1 = Instructional("Understanding the Universe", "Great Courses", 1, "Alex Filippenko", 180, "DVD")
>>> videos = []
>>> videos.append(m1)
>>> videos.append(i1)
>>> for video in videos:
... print(video)
... print(video.get_name())
...
2: Forbidden Planet, 98 minutes, DVD Directed by Fred McLeod Wilcox, MGM
Forbidden Planet
1: Understanding the Universe: Disc 1, Instructor Alex Filippenko
Understanding the Universe: Disc 1
- This is an example of a feature of inheritance
called polymorphism
Mutator for the Instructional Class
Accessors for the Instructional Class
Attendance
New Material
Functions Calling Functions
- A function
is a collection of statements that has a name ...
- and does some work
- To use a function we make a
function call
- Since a function call is a statement ...
- the code inside one function ...
- can call another function
- Let's look at an example
$ cat function_calling_function.py
#! /usr/bin/python3
# Demonstrates one function calling another
def function_1(arg):
print("This is function_1 printing it's argument:", arg)
print()
print("function_1 is now calling function_2(' + arg +')")
function_2(arg)
print()
print("This is function_1 signing off")
def function_2(arg):
print()
print("This is function_2 printing it's argument:", arg)
function_1("Hello")
- When we run this, we get
$ ./function_calling_function.py
This is function_1 printing it's argument: Hello
function_1 is now calling function_2("Hello")
This is function_2 printing it's argument: Hello
This is function_1 signing off
- Notice that when function_2 finishes it's work ...
- control reverts to function_1
Recursive Functions
- A function can call another function
- That means a function can call itself
- A function that calls itself is a
recursive function
- Here is an example of a recursive function
>>> def add_one_and_print(num):
... num +=1
... print(num)
... add_one_and_print(num)
...
- But if we call this function with an argument of 0 ...
- it will run for a long time
1
2
3
4
5
...
994
995
- But eventually it will print error message
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in add_one_and_print
File "<stdin>", line 4, in add_one_and_print
File "<stdin>", line 4, in add_one_and_print
[Previous line repeated 992 more times]
File "<stdin>", line 3, in add_one_and_print
RecursionError: maximum recursion depth exceeded while calling a Python object
996
- The problem is that the function was not told when to stop
- You might think that it should go on counting forever ...
- until I stopped it by hitting Control C
- But this did not happen
- Instead the Python interpreter stopped the script
- Why?
- Every time a function calls another function ...
- a little bit of RAM must be used to store the function's
local variables
- These allocations of RAM are removed when the function ends
- But in this case the function never finished
- So the interpreter kept allocating RAM ...
- until it ran out
- When that happened, it aborted the script
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
under a certain condition
- A recursive function call that approaches
this end condition
- The condition that causes the program to end is called the
base case
- Every time the function makes a recursive call to itself ...
- the argument to the function call must bring it closer to the base case
- We can illustrate this with a function to calculate factorials
- The factorial of n is written as
n!
- It is defined as product of multiplying all numbers from 1 to n
- So the factorial of 2 is
2! = 1 * 2 = 2
- Similarly
3! = 1 * 2 * 3 = 6
4! = 1 * 2 * 3 * 4 = 24
5! = 1 * 2 * 3 * 4 * 5 = 120
- If you look closely at these calculations you will see a pattern
3! = 2! * 3
4! = 3! * 4
5! = 4! * 5
- Here is the formula for factorial
n! = (n-1)! * n
- What is the condition that will cause this recursion to stop?
- I left out a small part of the definition of factorial
- It only works for integers greater than 1
- The factorial of 1 has a different value
1! = 1
- With this in mind, we can write a recursive factorial function
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
- In this code the base case occurs if the number is 1
- Every call to the function will approach the base case
- Because the argument of each function call ...
- is one less than the original argument used to call the function
Calculating Fibonacci Numbers
- The Fibonacci numbers are a sequence of integers
- The first two numbers are
1 1
- The next number is the sum of the preceding two numbers
- Here are the first ten Fibonacci numbers
1 1 2 3 5 8 13 21 34 55
- Here's the formula for the nth Fiboacci number in the sequence
F(n) = F(n - 1) + F(n - 2)
- But this definition only works if we further specify
F(1) = 1
F(2) = 1
- They are the base cases
- Here is a recursive function to calculate the nth Fibonacci number
def fibonacci(num):
if num == 1 or num == 2:
return 1
else:
return fibonacci(num - 1) + fibonacci(num - 2)
- To get the Fibonacci sequence ...
- I have to call this function inside a
for
loop
for n in range(1, 11):
print(fibonacci(n), end=" ")
- And I get
1 1 2 3 5 8 13 21 34 55
Palindromes
- Palindromes are words or phrases that read the same backward
and forward
- Here are some words that are palindromes
- We can create function that tells whether a string is a palindrome ...
- using the following recursive algorithm
if length of the string is 0 or 1:
return true
else if the first character and the last character are the same:
return a recursive call to the function using the string stripped of 1st and last character
else:
return false
- Here is the function
def palindrome(s):
if len(s) == 0 or len(s) == 1:
return True
elif s[0] == s[-1]:
return palindrome(s[1:-1])
else:
return False
- When we use it, we see it works
>>> palindrome('rotator')
True
>>> palindrome('roxator')
False
- Phrases can also be palindromes
- But when testing we must ignore spaces ...
- and captialization
- To do this we can write a function
def spaces_remove(phrase):
phrase = phrase.lower()
new_phrase = ''
for ch in phrase:
if ch != ' ':
new_phrase += ch
return new_phrase
- Which we can use to test phrases
>>> phrase = "A man a plan a canal Panama"
>>> print(phrase)
A man a plan a canal Panama
>>> phrase = spaces_remove(phrase)
>>> print(phrase)
amanaplanacanalpanama
>>> print(palindrome(phrase))
True
Using Recursion in IT
- Many things in mathematics are defined recursively ...
- like factorials and Fibonacci_numbers
- But recursion also appears in IT
- The
hierarchical filesystem
is recursive
- There is a special directory at the top called
root
...
- that contains other directories ...
- each of which in turn contains other directories ...
- and so on
- Sometimes you need to go through all the directories ...
- looking at what they contain
- This is called a directory walk
- And it is a recursive problem
- Let's write a program to count the total number of directories ...
- starting at some point
- Here is the algorithm
set count to 0
get a list of all the entries in the current directory
for each entry
if the entry is a directory
increase count by 1
run the function on this new directory
and increase count by what it returns
return the count
- Here is the code
def dir_count(path):
count = 0
entries = os.listdir(path)
for entry in entries:
entry_path = path + "/" + entry
if os.path.isdir(entry_path):
count += 1
count += dir_count(entry_path)
return count
Binary Search
- Recursion can also be used to find a value in a sorted list
- The basic idea is to keep dividing the list in half ...
- until you get the index of the value you want
- Here is the code for a binary binary search function
def binary_search(nums, low, high, value):
if high >= low:
middle_index = (high + low) // 2
# if value is present at the middle_index itself
if nums[middle_index] == value:
return middle_index
# if value is smaller than middle_index value, then it can only
# be present in left of the list
elif nums[middle_index] > value:
return binary_search(nums, low, middle_index - 1, value)
# otherwise the value can only be present in right of the list
else:
return binary_search(nums, middle_index + 1, high, value)
else:
# value is not present in the list
return -1
- Consider the following list with indexes above the values
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
[12, 14, 15, 18, 23, 25, 27, 28, 30, 35, 36, 40, 42, 44, 46, 48, 50]
- This list has 17 elements
- Let's say we want the index of the element whose value is 25
- We call the function like this
binary_search(num_list, 0, 16, 25)
- The function computes the middle index value like this
middle_index = (16 + 0) // 2 # result is 8
- The value at index 8 is 30
- 30 is bigger than 25
- So now we repeat the process on the smaller list to the left of 30
0 1 2 3 4 5 6 7
[12, 14, 15, 18, 23, 25, 27, 28]
- The recursive call is
binary_search(num_list, 0, 7, 25)
- The function computes the middle index
middle_index = (7 + 0) // 2 # result is 3
- The value at index 3 is 18
- 18 is smaller than 25
- So now we repeat the process on the smaller list to the right of 18
4 5 6 7
[23, 25, 27, 28]
- The recursive call is
binary_search(num_list, 4, 7, 25)
- Again we compute middle_index
middle_index = (7 + 4) // 2 # result is 5
- The value at index 5 is 25
- Which we return
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 ...
- 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
Calculating Factorials with a Loop
- It is very easy to calculate factorials with a loop
- Here is the algorithm
set value to 1
set count to 1
while count is less than number
increment count
set value to count times value
return the value
- Turning this into Python code we get
def factorial(number):
value = 1
count = 1
while count < number:
count += 1
value *= count
return value
- Calling the function, I get
>>> print("5!:",factorial(5))
5!: 120
Calculating Fibonacci Numbers with a Loop
- We can also calculate a Fibonacci number with a loop
- Here is the algorithm
if number is 1
return 1
else:
set first to 0
set second to 1
set count to 1
while count is less than number
increment count by 1
set value to first plus second
set first to second
set second to value
return value
- Turning this into code, we get
def fibonacci(number):
if number == 1:
return 1
else:
first = 0
second = 1
count = 1
while count < number:
count += 1
value = first + second
first = second
second = value
return value
- Compare this to the code for the recursive version
def fibonacci(num):
if num == 1 or num == 2:
return 1
else:
return fibonacci(num - 1) + fibonacci(num - 2)
- The loop version is longer ...
- and it took me a long time to figure out
Direct versus Indirect Recursion
- Recursion
is where something refers to itself
- The functions above refer to themselves directly
- This is called
direct recursion
- But you can also have
indirect recursion
- In indirect recursion something defines itself in term of something else
- And that something else defines itself using the original item
- So if function A called function
B
- And function B called function
A
- This 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
Intellectual Property
Class Exercise
Class Quiz