IT 117: Intermediate Scripting
Class 24
Review
New Material
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.
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 the class name
Video.__init__(...)
- The 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
- At this point we need to 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 Video
- Not in Movie
- 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
connects 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 class
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
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
- Because 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
- Code that works toward 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
- The definition for factorial given above 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 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
Another Example of Recursion
- Many things in mathematics are defined recursively
- Like factorials and Fibonacci_numbers
- But recursion also appears in IT
- The
hierarchical filesystem
is recursive
- The special directory at the top called
root
contains directories
- Each of which in turn contains other directories
- And so on
- Sometimes you need to go through all the directores
- 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 number of directories
- Starting at some point
- Here is the algorithm
set a counter to 0
get a list of all the entries in the current directory
for each entry
if the entry is a directory
increase the count by 1
run the function on this new directory
and increase the 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
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 a factorial variable to 1
set a count variable to 1
while count is less than the number whose factorial we are computing
increment count
set factorial to count times the current value of factorial
return the factorial 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 the number is 1
return 1
else:
set the first number variable to 0
set the second number variable to 1
set count to 1
while count is less than the number
increment count by 1
set value to first number plus the second number
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
Attendance
Class Exercise
Class Quiz