CS1010S Lecture 10 - Memoization, Dynamic Programming & Exceptions
CS1010S Lecture 10 - Memoization, Dynamic Programming & Exceptions
Lecture 10
Memoization, Dynamic
Programming & Exception Handling
26 Oct 2022
Recap: OOP Concepts
• Class vs Instance
- Class is a template, which is used to create instances
pusheen = Cat("Pusheen")
Cat Cat
name name: 'Pusheen'
pusheen
lives lives: 9
say() say()
socCat
Cat
name: 'SoCCat'
lives: 9
soCCat = Cat("SoCCat")
say()
Recap: Class
• Contains class Cat:
def __init__(self, name):
1. Fields/Properties
self.name = name
2. Methods/Functions self.lives = 9
override is-a
class Cat(Mammal):
Cat def __init__(self, name):
super().__init__(name)
lives
self.lives = 9
say() def say(self):
scratch() print("meow")
def scratch(self):
print(self.name,
"scratch!")
Recap: Inheritance
Mammal >>> pusheen = Cat("Pusheen")
>>> pusheen.say()
name
meow
say()
inherits >>> pusheen.scratch()
Pusheen scratch!
override is-a
Cat >>> pusheen.lives
9
lives
say()
scratch()
Recap: super()
Mammal super() refers to superclass
name self refers to instance
say()
class Cat(Mammal):
is-a super()
…
Cat def say(self):
lives super().say()
print("meow")
say()
scratch()
>>> pusheen.say()
self Pusheen say
meow
Polymorphism
• Which superclass does super() resolve to?
- Depends on the type of the object/instance
Speaker
say def lecture(self, stuff):
self.say(…)
>>> seth = Lecturer() Lecturer
>>> seth.lecture(…) seth lecture
seth.say(…)
Polymorphism
• Which superclass does super() resolve to?
- Depends on the type of the object/instance
Speaker
say def lecture(self, stuff):
self.say(…)
>>> seth = Lecturer() Lecturer
>>> seth.lecture(…) lecture
Arrogant
>>> ben = ArrogantLecturer(…)
Lecturer def say(self, stuff):
>>> ben.lecture(…)
ben say super().say(…)
ben.say()
Speaker
say
Lecturer
lecture
Arrogant Singer def say(self, stuff):
def say(self, stuff): Lecturer say super().say(…)
super().say(…)
say sing
Singing
Arrogant
ben.say()
ben Lecturer
Today’s Agenda
• Optional Arguments
See recording
• Exception Handling
• Memoization
• Dynamic Programming
Errors and Exceptions
Errors and Exceptions
• Until now error messages haven’t been more than
mentioned, but you have probably seen some
• Two kinds of errors (in Python):
1. syntax errors
2. exceptions
Syntax Errors
>>> while True print('Hello world')
SyntaxError: invalid syntax
Exceptions
• Errors detected during execution are called
exceptions
• Examples:
- ZeroDivisonError,
- NameError,
- TypeError
ZeroDivisionError
>>> 10 * (1/0)
Traceback (most recent call last):
File "<pyshell#3>", line 1, in <module>
10 * (1/0)
ZeroDivisionError: division by zero
NameError
>>> 4 + spam*3
Traceback (most recent call last):
File "<pyshell#4>", line 1, in <module>
4 + spam*3
NameError: name 'spam' is not defined
TypeError
>>> '2' + 2
Traceback (most recent call last):
File "<pyshell#5>", line 1, in <module>
'2' + 2
TypeError: Can't convert 'int' object to str
implicitly
ValueError
>>> int('one')
Traceback (most recent call last):
File "<pyshell#2>", line 1, in <module>
int('one')
ValueError: invalid literal for int() with base 10:
'one'
Handling Exceptions
The simplest way to catch and handle exceptions is with a try-
except block:
x, y = 5, 0
try:
z = x/y
except ZeroDivisionError:
print("divide by zero")
Try-Except (How it works I)
• The try clause is executed
• If an exception occurred, skip the rest of the try
clause, to a matching except clause
• If no exception occurs, the except clause is skipped
(go to the else clause, if it exists)
• The finally clause is always executed before
leaving the try statement, whether an exception has
occurred or not.
Try-Except
• A try clause may have more than 1 except clause,
to specify handlers for different exception.
• At most one handler will be executed.
• Similar with if-elif-else
• finally will always be executed
Try-Except
try:
try:
# statements except Error1:
except Error1:
# handle error 1 #handle error 1
except Error2:
# handle error 2 except Error2:
except: # wildcard
# handle generic error else: #handle error 2
else:
# no error raised except:
finally: finally:
#handle
# always executed
generic error
Try-Except Example
def divide_test(x, y):
try:
result = x / y
except ZeroDivisionError:
print("division by zero!")
else:
print("result is", result)
finally:
print("executing finally clause")
Try-Except Blocks
>>> divide_test(2, 1) def divide_test(x, y):
try:
result is 2.0
result = x / y
executing finally clause
except ZeroDivisionError:
print("division by zero!")
>>> divide_test(2, 0)
else:
division by zero! print("result is", result)
executing finally clause finally:
print("executing finally
>>> divide_test("2", "1") clause")
executing finally clause
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "<stdin>", line 3, in divide
TypeError: unsupported operand type(s) for /: 'str' and 'str'
Raising Exceptions
The raise statement allows the programmer to force a specific
exception to occur:
raise MyError('oops!')
Traceback (most recent call last):
File "<stdin>", line 1, in ?
__main__.MyError: 'oops!'
Why use Exceptions?
In the good old days of C, many procedures
returned special ints for special conditions,
i.e. -1
Why use Exceptions?
• But Exceptions are better because:
- More natural
- More easily extensible
- Nested Exceptions for flexibility
Recall: Fibonacci
def fib(n):
if n < 2:
return n
else:
return fib(n-1) + fib(n-2)
Time complexity =
How can we do better? 𝑂(Φ𝑛) (exponential!)
Computing Fibonacci
fib(5)
fib(4) fib(3)
1 1 0 1 0
fib(1) fib(0)
1 0 Lots of duplicates
What’s the obvious
way to do better?
Remember what you
had earlier computed!
Memoization
Notice the spelling,
NOT memorization
Simple Idea!!
Function records, in a table, values that have previously been
computed.
• A memoized function:
- Maintains a table in which values of previous calls are
stored
- Use the arguments that produced the values as keys
• When the memoized function is called, check table to
see if the value exists:
- If so, return value.
- Otherwise, compute new value in the ordinary way and
store this in the table.
Memoized Fib
def memo_fib(n):
table = {} Does not work!
def helper(n): New table is created at
if n < 2: each call!
return n
elif n in table: This function is our
return table[n] memoized fib
else:
table[n] = helper(n-1) + helper(n-2)
return table[n]
return helper(n)
Memoized Fib
def memo_fib():
Change to a “creator” function
table = {}
def helper(n):
if n < 2: Return the memoized fib
return n
elif n in table:
return table[n]
else:
table[n] = helper(n-1) + helper(n-2)
return table[n]
return helper Define fib using the creator
fib = memo_fib()
Computing Fibonacci
memo_fib(5)
memo_fib(4) memo_fib(3)
memo_fib(3) memo_fib(2)
memo_fib(2) memo_fib(1)
memo_fib(1) memo_fib(0)
1 0
Not “computed” but
by table lookup!
Compare to the Original Version
fib(5)
fib(4) fib(3)
1 1 0 1 0
fib(1) fib(0)
1 0 Lots of duplicates
Memoize any function
def memoize(fn): Write a “creator” function
table = {}
Use saved value
def helper(n):
if n in table:
Store value Does not work!
return table[n]
Restricts fn to functions
else:
with only 1 input
table[n] = fn(n)
return table[n]
return helper Define using the creator
fn = memoize(fn)
The * notation
• Call a function for which you don't know in advance
how many arguments there will be using.
def get_description(choc):
return choc[0]
def get_weight(choc):
return choc[1]
def get_value(choc):
return choc[2]
Here are the Chocolates
shirks_chocolates =
(make_chocolate('caramel dark', 13, 10),
make_chocolate('caramel milk', 13, 3),
make_chocolate('cherry dark', 21, 3),
make_chocolate('cherry milk', 21, 1),
make_chocolate('mint dark', 7, 3),
make_chocolate('mint milk', 7, 2),
make_chocolate('cashew-cluster dark', 8, 6),
make_chocolate('cashew-cluster milk', 8, 4),
make_chocolate('maple-cream dark', 14, 1),
make_chocolate('maple-cream milk', 14, 1))
Implementing a Box
def make_box(list_of_choc, weight, value):
return (list_of_choc, weight, value)
def make_empty_box():
return make_box((), 0, 0)
def box_chocolates(box):
return box[0]
def box_weight(box):
return box[1]
def box_value(box):
return box[2]
Implementing a Box
def add_to_box(choc, box):
return make_box(
box_chocolates(box) + (choc,),
box_weight(box) + get_weight(choc),
box_value(box) + get_value(choc))
• Think memoization!
Original Simple Solution
def pick(chocs, weight_limit):
if chocs==() or weight_limit==0:
return make_empty_box()
elif get_weight(chocs[0]) > weight_limit:
return pick(chocs[1:], weight_limit)
else:
box1 = pick(chocs[1:], weight_limit)
box2 = add_to_box(chocs[0],
pick(chocs,
weight_limit -
get_weight(chocs[0])))
return better_box(box1, box2)
Memoized Version
def memo_pick(chocs, weight_limit):
def helper(chocs, weight_limit):
if chocs==() or weight_limit==0:
return make_empty_box()
elif get_weight(chocs[0]) > weight_limit:
return memo_pick(chocs[1:], weight_limit)
else:
box1 = memo_pick(chocs[1:], weight_limit)
box2 = add_to_box(chocs[0],
memo_pick(chocs,
weight_limit -
get_weight(chocs[0])))
return better_box(box1, box2)
return memoize(helper, "pick")(chocs, weight_limit)
Recap: Memoization
• Two Steps:
1. Write function to perform required computation
2. Add wrapper that stores the result of the computation
(𝑂(1) lookup table)
n+1
table.append(
1 1 1 1 1
row.copy()) 1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
𝑛1 1 1 1 1 𝑛
𝐶 0 𝐶𝑘
Let’s check out the table
j
for j in range(1,k+1): 1 0 0 0 0
table[0][j] = 0
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
Let’s check out the table
for j in range(1,k+1): 1 0 0 0 0
table[0][j] = 0
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
Let’s check out the table
j
for i in range(1,n+1): 1 0 0 0 0
for j in
i
1 1 1 1 1
range(1,k+1):
table[i][j] =
1 1 1 1 1
table[i-1][j-1] + 1 1 1 1 1
table[i-1][j] 1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
Let’s check out the table
j
for i in range(1,n+1): 1 0 0 0 0
for j in
i
1 1 0 1 1
range(1,k+1):
table[i][j] =
1 1 1 1 1
table[i-1][j-1] + 1 1 1 1 1
table[i-1][j] 1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
Let’s check out the table
j
for i in range(1,n+1): 1 0 0 0 0
for j in
i
1 1 0 0 1
range(1,k+1):
table[i][j] =
1 1 1 1 1
table[i-1][j-1] + 1 1 1 1 1
table[i-1][j] 1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
Let’s check out the table
j
for i in range(1,n+1): 1 0 0 0 0
for j in
i
1 1 0 0 0
range(1,k+1):
table[i][j] =
1 1 1 1 1
table[i-1][j-1] + 1 1 1 1 1
table[i-1][j] 1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
Let’s check out the table
j
for i in range(1,n+1): 1 0 0 0 0
for j in
i
1 1 0 0 0
range(1,k+1):
table[i][j] =
1 1 1 1 1
table[i-1][j-1] 1 1 1 1 1
+ table[i-1][j] 1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
Let’s check out the table
j
for i in range(1,n+1): 1 0 0 0 0
for j in
i
1 1 0 0 0
range(1,k+1):
table[i][j] =
1 2 1 1 1
table[i-1][j-1] + 1 1 1 1 1
table[i-1][j] 1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
Fast Forward
1 0 0 0 0
1 1 0 0 0
1 2 1 0 0
1 3 3 1 0
1 4 6 4 1
1 5 10 10 5
1 6 15 20 15
1 7 21 35 35
Can we adopt a dynamic
programming approach for solving
the chocolate packing problem?
()
…
Building the chocolate packing table
((A 4 2)
Weight Limit () ((A 4 2))
(B 5 3 ))
0 () () ()
1 () () ()
2 () () () Cannot
3 () () () add B
4 () (A) (A)
5 () (A) (B)
6 () (A)
7 () (A)
8 () (A,A)
()
…
…
Building the chocolate packing table
((A 4 2)
Weight Limit () ((A 4 2))
(B 5 3 ))
0 () () ()
1 () () ()
2 () () ()
3 () () ()
4 () (A) (A) +B
5 () (A) B
6 () (A) B +B
7 () (A) B
8 () (A,A)
…
()
…
Building the chocolate packing table
((A 4 2)
Weight Limit () ((A 4 2))
(B 5 3 ))
0 () () ()
1 () () ()
2 () () ()
3 () () ()
4 () (A) (A)
5 () (A) B
6 () (A) B
7 () (A) B +B
8 () (A,A) (A,A)
…
()
…
…
Challenge of the Day: Write dp_pick_chocolates over the weekend. ☺
Prime Numbers
• In recitation, we defined a function is_prime to
check whether a number is prime
• But what if we wanted to list ALL of the numbers that
are prime, in the interval [0, … , 𝑛]?
Prime Numbers: Naïve Solution
def is_prime(n): # O(n0.5)
if n==0 or n==1:
return False
elif n == 2:
return True
for i in range(2, int(sqrt(n))+1):
if n % i == 0:
return False
return True