Lab 10: Interfaces
Due at 11:59pm on 07/23/2015.
Starter Files
Download lab10.zip. Inside the archive, you will find starter files for the questions in this lab, along with a copy of the OK autograder.
Submission
By the end of this lab, you should have submitted the lab with
python3 ok --submit
. You may submit more than once before the
deadline; only the final submission will be graded.
- To receive credit for this lab, you must complete Questions 1, 2, 3, 4, 5, and 6 in lab10.py and submit through OK.
- Questions 7 is extra practice. It can be found in the lab10_extra.py file. It is recommended that you complete this problem on your own time.
Interfaces
In computer science, an interface is a shared set of attributes, along with a specification of the attributes' behavior. For example, an interface for vehicles might consist of the following methods:
def drive(self)
: Drives the vehicle if it has stopped.def stop(self)
: Stops the vehicle if it is driving.
Data types can implement the same interface in different ways. For example, a
Car
class and a Train
can both implement the interface described above, but the Car
probably has a different mechanism for drive
than the Train
.
The power of interfaces is that other programs don't have to know how each data type implements the interface — only that they have implemented the interface. The following travel
function can work with both Car
s and Train
s:
def travel(vehicle):
while not at_destination():
vehicle.drive()
vehicle.stop()
QueueVendingMachines
QueueVendingMachine
is an interface for vending machines that have a queue of items. Users have no choice in what they buy, they simply get the next item in the queue. This interface contains the methods dispense
and collect_money
. See the doctests below for details.
def dispense():
"""
Returns the next item queued in the vending machine, or None if there is no item left.
"""
def collect_money(item):
"""
Collects money earned from item and returns total money earned by the machine.
"""
Here is one class that implements QueueVendingMachine
called SnacksVendor
. It dispenses snacks with a set price.
class SnacksVendor(object):
def __init__(self, snacks, prices):
""" snacks is a list of all the snacks (strings) for sale, and can
contain multiple instances of the same snack type. prices is a
dictionary that maps each snack type (string) to its price (number)
.
"""
self.inventory = snacks
self.prices = prices
self.revenue = 0
def dispense(self):
""" Removes and dispenses the last snack from the inventory.
>>> vendor = SnacksVendor(['candy', 'chips', 'chips'],
... {'candy': 1, 'chips': 3})
>>> vendor.dispense()
'chips'
>>> vendor.dispense()
'chips'
>>> vendor.dispense()
'candy'
>>> no_snack_left = vendor.dispense()
>>> no_snack_left is None
True
"""
if self.inventory:
return self.inventory.pop()
def collect_money(self, item):
""" Adds the price of snack to total revenue and returns the total
revenue.
>>> vendor = SnacksVendor(['candy', 'chips'], {'candy': 1, 'chips': 3})
>>> snack = vendor.dispense()
>>> vendor.collect_money(snack)
3
>>> vendor.collect_money(vendor.dispense())
4
"""
self.revenue += self.prices[item]
return self.revenue
Question 1: MixedJuiceVendor
Now fill in another class, MixedJuiceVendor
, that also implements QueueVendingMachine
. MixedJuiceVendor
keeps an instance attribute self.fruits
, a list of fruits, and creates and dispenses a drink made with the first two fruits in the inventory. It also keeps an inventory of self.cups
, and each drink takes one cup to produce.
Write the dispense
and collect_money
methods. MixedJuiceVendor
is a waste-free vending machine, so do not perform any operations that create new lists of fruits (i.e. use mutation!). See the doctests for details.
class MixedJuiceVendor(object):
""" A QueueVendingMachine that vends mixed juices.
>>> vendor = MixedJuiceVendor(['kiwi', 'mango', 'apple', 'guava'], 3)
>>> juice = vendor.dispense()
>>> juice
'kiwi-mango'
>>> vendor.collect_money(juice)
9
>>> juice = vendor.dispense()
>>> juice
'apple-guava'
>>> vendor.collect_money(juice)
19
>>> vendor.cups
1
>>> juice = vendor.dispense() # no fruits left!
>>> print(juice)
None
>>> vendor2 = MixedJuiceVendor(['guava', 'mango'], 0)
>>> juice = vendor2.dispense() # no cups!
>>> print(juice)
None
>>> vendor3 = MixedJuiceVendor(['lemon'], 1)
>>> juice = vendor3.dispense() # only one fruit!
>>> print(juice)
None
"""
def __init__(self, fruits, cups):
""" fruits is a list of fruits in the inventory. cups is the number of
cups left to put juice in.
"""
self.fruits = fruits
self.cups = cups
self.revenue = 0
def dispense(self):
""" Dispenses a mixed juice combining the first two fruits in the
fruit inventory. Juices can only be created if there are at least
two fruits left and there is at least one cup left.
"""
"*** YOUR CODE HERE ***"
if len(self.fruits) >= 2 and self.cups:
self.cups -= 1
return self.fruits.pop(0) + "-" + self.fruits.pop(0)
def collect_money(self, item):
""" Each juice is priced based on how many letters make up its two
fruits.
"""
"*** YOUR CODE HERE ***"
self.revenue = self.revenue + len(item) - 1
return self.revenue
Hint: use the
pop(i)
method on a list to remove and retrieve the element at indexi
.>>> lst = [1, 2, 3, 4] >>> lst.pop() 4 >>> lst.pop(1) 2 >>> lst [1, 3]
Use OK to test your code:
python3 ok -q MixedJuiceVendor
Question 2: Total Revenue
A budding entrepreneur finds out about QueueVendingMachine
s, and, predicting that they're the next big thing in our increasingly automated society, wants to invest in them. However, he simply wants to find the one machine that generates the most revenue, and buy 1000 of them. He hires you to write a function total_revenue
that takes in a QueueVendingMachine
and return the total revenue it generates if it sells all its products.
def total_revenue(qvm):
""" Returns total possible revenue generated from qvm.
>>> juices = MixedJuiceVendor(['orange', 'mango', 'banana', 'guava'], 10)
>>> total_revenue(juices)
22
>>> snacks = SnacksVendor(['chips', 'ramen', 'pretzels', 'chips'],
... {'chips': 4, 'pretzels': 7, 'ramen': 10})
>>> total_revenue(snacks)
25
"""
"*** YOUR CODE HERE ***"
total = 0
item = qvm.dispense()
while item:
total = qvm.collect_money(item)
item = qvm.dispense()
return total
Use OK to test your code:
python3 ok -q total_revenue
Question 3: Max Revenue
Now write a function max_revenue
that takes in a list of QueueVendingMachine
s and returns the one with the highest total possible revenue. Use the function total_revenue
that you just wrote.
def max_revenue(lst_of_qvm):
""" Returns the QueueVendingMachine from lst_of_qvm that has the greatest
total possible revenue, or the first in the list if their total
revenues are equal.
>>> juices = MixedJuiceVendor(['orange', 'mango', 'banana', 'guava'], 10)
>>> snacks = SnacksVendor(['chips', 'ramen', 'pretzels', 'chips'],
... {'chips': 4, 'pretzels': 7, 'ramen': 10})
>>> best = max_revenue([juices, snacks])
>>> best is snacks
True
"""
"*** YOUR CODE HERE ***"
best, max_rev = None, -1
for qvm in lst_of_qvm:
total_rev = total_revenue(qvm)
if total_rev > max_rev:
max_rev, best = total_rev, qvm
return best
#Alternate solution
def max_revenue(lst_of_qvm):
return max(lst_of_qvm, key=lambda qvm : total_revenue(qvm))
Use OK to test your code:
python3 ok -q max_revenue
Magic Methods
Python defines many interfaces that can interact with built-in operators. These interfaces contain magic methods, which can be implemented by user-defined classes. Magic methods are surround by two underscores.
The interface for arithmetic includes the following methods:
def __add__(self, other):
Allows objects to doself + other
.def __sub__(self, other):
Allows objects to doself - other
.def __mul__(self, other):
Allows objects to doself * other
.
In addition, the sequence interface is defined by the following magic methods:
def __len__(self):
Allows objects to dolen(self)
.def __getitem__(self, index):
Allows objects to doself[i]
.
Two built-in data types that implement the sequence interface are lists and dictionaries.
In lecture, you saw the sequence interface being implemented for a user-defined data type, Linked Lists.
class Link:
"""A linked list.
>>> s = Link(1, Link(2, Link(3, Link(4))))
>>> len(s)
4
>>> s[2]
3
>>> s
Link(1, Link(2, Link(3, Link(4))))
"""
empty = ()
def __init__(self, first, rest=empty):
assert rest is Link.empty or isinstance(rest, Link)
self.first = first
self.rest = rest
def __len__(self):
return 1 + len(self.rest)
def __getitem__(self, i):
if i == 0:
return self.first
else:
return self.rest[i-1]
def __repr__(self):
if self.rest is not Link.empty:
rest_str = ', ' + repr(self.rest)
else:
rest_str = ''
return 'Link({0}{1})'.format(repr(self.first), rest_str)
Question 4: is_palindrome
Write the function is_palindrome
such that it works for any data type that
implements the sequence interface.
def is_palindrome(seq):
""" Returns True if the sequence is a palindrome. A palindrome is a sequence
that reads the same forwards as backwards
>>> is_palindrome(Link("l", Link("i", Link("n", Link("k")))))
False
>>> is_palindrome(["l", "i", "n", "k"])
False
>>> is_palindrome("link")
False
>>> is_palindrome(Link.empty)
True
>>> is_palindrome([])
True
>>> is_palindrome("")
True
>>> is_palindrome(Link("a", Link("v", Link("a"))))
True
>>> is_palindrome(["a", "v", "a"])
True
>>> is_palindrome("ava")
True
"""
"*** YOUR CODE HERE ***"
for i in range(len(seq)//2):
if seq[i] != seq[len(seq) - 1 - i]:
return False
return True
Use OK to test your code:
python3 ok -q is_palindrome
Question 5: __mul__
Let's implement __mul__
for our Link
class, which allows us to scalar
multiply a Linked List. Look to the doctest for its behavior.
class Link:
# Link implementation hidden for space
def __mul__(self, c):
""" Scalar multiplies self by c. Raise AssertionError if c is not a
number.
>>> (Link.empty * 3) is Link.empty
True
>>> a = Link(1, Link(5))
>>> b = a * 1
>>> b
Link(1, Link(5))
>>> b is not a
True
>>> b = a * 3
>>> b
Link(1, Link(5, Link(1, Link(5, Link(1, Link(5))))))
>>> a is not b
True
>>> (a * 0) is Link.empty
True
>>> a * Link(3)
AssertionError
"""
"*** YOUR CODE HERE ***"
assert type(c) is int
def helper(lst, count):
if count == 0:
return Link.empty
if lst is Link.empty:
return helper(self, count - 1)
return Link(lst.first, helper(lst.rest, count))
return helper(self, c)
# Alternate Solution
def __mul__(self, c):
assert type(c) == int
if c == 0:
return Link.empty
rest = self * (c - 1)
new, self = Link(self.first), self.rest
end = new
while self is not Link.empty:
end.rest, self = Link(self.first), self.rest
end = end.rest
end.rest = rest
return new
Use OK to test your code:
python3 ok -q mul
Exceptions
You've learned how to raise exceptions, but it's also important to catch exceptions when necessary. Instead of letting the exception propogate back to the user, we can catch it using a try...except
block and allow the program to continue.
try:
<try suite>
except Exception as e:
<except suite>
We put the code that might raise an exception in the <try suite>
. If an exception of type Exception
is raised, then the program will skip the rest of that suite and execute the <except suite>
. Generally, we want to be specify exactly which Exception
we want to handle, such as TypeError
or ZeroDivisionError
.
Notice that we can catch the exception as e
. This assigns the exception object to the variable e
. This can be helpful when we want to use information about the exception that was raised.
>>> try:
... 1/0
... raise ValueError # this line is not executed!
... except ZeroDivisionError as e:
... print('The try block raised a ' + str(e) + ' error.')
The try block raised a division by zero error.
Question 6: No KeyErrors Allowed
If we try to look up a key that does not exist in a dictionary, then
Python will raise a KeyError
. Write the function avoid_keyerror
which returns
returns the value mapped to key
in the dictionary
. If key
does
not exist, print 'Avoid Exception' and map key
to the string 'no value'.
def avoid_keyerror(dictionary, key):
""" Returns the value associated with key in dictionary. If key
does not exist in the dictionary, print out 'Avoid Exception' and
map it to the string 'no value'.
>>> d = {1: 'one', 3: 'three', 5: 'five'}
>>> avoid_keyerror(d, 3)
'three'
>>> avoid_keyerror(d, 4)
Avoid Exception
>>> d[4]
'no value'
"""
"*** YOUR CODE HERE ***"
try:
return dictionary[key]
except KeyError as e:
print("Avoid Exception")
dictionary[key] = 'no value'
Use OK to test your code:
python3 ok -q avoid_keyerror
Extra Questions
The following question is for extra practice — it can be found in the the lab10_extra.py file. It is recommended that you complete this problem on your own time.
Question 7: __delitem__
Implement __delitem__
for our Link
class, which allows us
to delete an index from a Link
using this syntax: del lst[i]
.
class Link:
# Link implementation hidden for space
def __delitem__(self, index):
""" Deletes value at given index from the Link. Raise a ValueError if the
length of the Linked List is 1. Raise an IndexError if the index is out
of bounds.
>>> s = Link(2, Link(4, Link(6, Link(8))))
>>> del s[3]
>>> s
Link(2, Link(4, Link(6)))
>>> del s[4]
Traceback (most recent call last):
...
IndexError
>>> del s[0]
>>> s
Link(4, Link(6))
>>> del s[0]
>>> s
Link(6)
>>> del s[0]
Traceback (most recent call last):
...
ValueError
"""
"*** YOUR CODE HERE ***"
if len(self) == 1:
raise ValueError
if len(self) <= index:
raise IndexError
if index == 0:
self.first, self.rest= self.rest.first, self.rest.rest
elif index == 1:
self.rest = self.rest.rest
else:
del self.rest[index - 1]
Use OK to test your code:
python3 ok -q __delitem__