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 superconnects 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 forloop
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 PropertyAttendance
	
	Class Exercise
	
	Class Quiz