Homework 6
Due by 11:59pm on Wednesday, 7/20
Instructions
Download hw06.zip. Inside the archive, you will find a file called hw06.py, along with a copy of the OK autograder.
Submission: When you are done, submit with python3 ok
--submit
. You may submit more than once before the deadline; only the
final submission will be scored. See Lab 0 for instructions on submitting
assignments.
Using OK: If you have any questions about using OK, please refer to this guide.
Readings: You might find the following references useful:
Mutation
Question 1: Password Protected Account
In lecture, we saw how to use functions to create mutable objects.
Here, for example, is the function make_withdraw
which produces a
function that can withdraw money from an account:
def make_withdraw(balance):
"""Return a withdraw function with BALANCE as its starting balance.
>>> withdraw = make_withdraw(1000)
>>> withdraw(100)
900
>>> withdraw(100)
800
>>> withdraw(900)
'Insufficient funds'
"""
def withdraw(amount):
nonlocal balance
if amount > balance:
return 'Insufficient funds'
balance = balance - amount
return balance
return withdraw
Write a version of the make_withdraw
function that returns
password-protected withdraw functions. That is, make_withdraw
should
take a password argument (a string) in addition to an initial balance.
The returned function should take two arguments: an amount to withdraw
and a password.
A password-protected withdraw
function should only process
withdrawals that include a password that matches the original. Upon
receiving an incorrect password, the function should:
- Store that incorrect password in a list, and
- Return the string 'Incorrect password'.
If a withdraw function has been called three times with incorrect
passwords p1
, p2
, and p3
, then it is locked. All subsequent
calls to the function should return:
"Your account is locked. Attempts: [<p1>, <p2>, <p3>]"
The incorrect passwords may be the same or different:
def make_withdraw(balance, password):
"""Return a password-protected withdraw function.
>>> w = make_withdraw(100, 'hax0r')
>>> w(25, 'hax0r')
75
>>> w(90, 'hax0r')
'Insufficient funds'
>>> w(25, 'hwat')
'Incorrect password'
>>> w(25, 'hax0r')
50
>>> w(75, 'a')
'Incorrect password'
>>> w(10, 'hax0r')
40
>>> w(20, 'n00b')
'Incorrect password'
>>> w(10, 'hax0r')
"Your account is locked. Attempts: ['hwat', 'a', 'n00b']"
>>> w(10, 'l33t')
"Your account is locked. Attempts: ['hwat', 'a', 'n00b']"
"""
"*** YOUR CODE HERE ***"
Use OK to test your code:
python3 ok -q make_withdraw
Question 2: Joint Account
Suppose that our banking system requires the ability to make joint
accounts. Define a function make_joint
that takes three arguments.
- A password-protected
withdraw
function, - The password with which that
withdraw
function was defined, and - A new password that can also access the original account.
The make_joint
function returns a withdraw
function that provides
additional access to the original account using either the new or old
password. Both functions draw down the same balance. Incorrect
passwords provided to either function will be stored and cause the
functions to be locked after three wrong attempts.
Hint: The solution is short (less than 10 lines) and contains no string
literals! The key is to call withdraw
with the right password and amount,
then interpret the result. You may assume that all failed attempts to withdraw
will return some string (for incorrect passwords, locked accounts, or
insufficient funds), while successful withdrawals will return a number.
Use type(value) == str
to test if some value
is a string:
def make_joint(withdraw, old_password, new_password):
"""Return a password-protected withdraw function that has joint access to
the balance of withdraw.
>>> w = make_withdraw(100, 'hax0r')
>>> w(25, 'hax0r')
75
>>> make_joint(w, 'my', 'secret')
'Incorrect password'
>>> j = make_joint(w, 'hax0r', 'secret')
>>> w(25, 'secret')
'Incorrect password'
>>> j(25, 'secret')
50
>>> j(25, 'hax0r')
25
>>> j(100, 'secret')
'Insufficient funds'
>>> j2 = make_joint(j, 'secret', 'code')
>>> j2(5, 'code')
20
>>> j2(5, 'secret')
15
>>> j2(5, 'hax0r')
10
>>> j2(25, 'password')
'Incorrect password'
>>> j2(5, 'secret')
"Your account is locked. Attempts: ['my', 'secret', 'password']"
>>> j(5, 'secret')
"Your account is locked. Attempts: ['my', 'secret', 'password']"
>>> w(5, 'hax0r')
"Your account is locked. Attempts: ['my', 'secret', 'password']"
>>> make_joint(w, 'hax0r', 'hello')
"Your account is locked. Attempts: ['my', 'secret', 'password']"
"""
"*** YOUR CODE HERE ***"
Use OK to test your code:
python3 ok -q make_joint
Objects
Question 3: Vending Machine
Create a class called VendingMachine
that represents a vending
machine for some product. A VendingMachine
object returns strings
describing its interactions. See the doctest below for examples:
class VendingMachine:
"""A vending machine that vends some product for some price.
>>> v = VendingMachine('candy', 10)
>>> v.vend()
'Machine is out of stock.'
>>> v.restock(2)
'Current candy stock: 2'
>>> v.vend()
'You must deposit $10 more.'
>>> v.deposit(7)
'Current balance: $7'
>>> v.vend()
'You must deposit $3 more.'
>>> v.deposit(5)
'Current balance: $12'
>>> v.vend()
'Here is your candy and $2 change.'
>>> v.deposit(10)
'Current balance: $10'
>>> v.vend()
'Here is your candy.'
>>> v.deposit(15)
'Machine is out of stock. Here is your $15.'
>>> w = VendingMachine('soda', 2)
>>> w.restock(3)
'Current soda stock: 3'
>>> w.deposit(2)
'Current balance: $2'
>>> w.vend()
'Here is your soda.'
"""
"*** YOUR CODE HERE ***"
Use OK to test your code:
python3 ok -q VendingMachine
Question 4: Miss Manners
Create a class called MissManners
that promotes politeness among our
objects. A MissManners
object takes another object on construction.
It has one method, called ask
. It responds by calling methods on the
object it contains, but only if the caller said please first.
Hint: Your implementation will need to use the *args
notation that
allows functions to take a flexible number of arguments.
Hint: Use getattr
and hasattr
to manipulate attributes using strings.
class MissManners:
"""A container class that only forward messages that say please.
>>> v = VendingMachine('teaspoon', 10)
>>> v.restock(2)
'Current teaspoon stock: 2'
>>> m = MissManners(v)
>>> m.ask('vend')
'You must learn to say please first.'
>>> m.ask('please vend')
'You must deposit $10 more.'
>>> m.ask('please deposit', 20)
'Current balance: $20'
>>> m.ask('now will you vend?')
'You must learn to say please first.'
>>> m.ask('please hand over a teaspoon')
'Thanks for asking, but I know not how to hand over a teaspoon.'
>>> m.ask('please vend')
'Here is your teaspoon and $10 change.'
>>> really_fussy = MissManners(m)
>>> really_fussy.ask('deposit', 10)
'You must learn to say please first.'
>>> really_fussy.ask('please deposit', 10)
'Thanks for asking, but I know not how to deposit.'
>>> really_fussy.ask('please please deposit', 10)
'Thanks for asking, but I know not how to please deposit.'
>>> really_fussy.ask('please ask', 'please deposit', 10)
'Current balance: $10'
"""
"*** YOUR CODE HERE ***"
Use OK to test your code:
python3 ok -q MissManners
Extra Questions: Implementing an Object System
Extra questions are not worth extra credit and are entirely optional. They are designed to challenge you to think creatively!
Question 5: Building OOP!
Object-oriented programming is great, but it's not magic. In fact, we can implement it using just functions and dictionaries! Let's explore this idea.
We can represent both classes and instances as dispatch dictionaries, a dictionary that maps strings representing commands to different functions.
Our classes will have three commands: get
retrieves a class attribute or
method, set
creates or overwrites a class attribute or method, and new
creates a new instance of the class. The make_class
function returns this
dispatch dictionary, and has been implemented for you.
def make_class(attributes={}):
def get_value(name):
if name in attributes: # name is a class attribute
return attributes[name]
else:
return None
def set_value(name, value):
attributes[name] = value
def __new__(*args):
instance = make_instance(cls)
return init_instance(instance, *args)
cls = {'get': get_value, 'set': set_value, 'new': __new__}
return cls
Our instances are dispatch dictionaries with two commands: get
retrieves an
instance or class attribute or bound method, and set
creates or overwrites an
instance attribute or bound method.
First, we implement the bind_method
function, which takes in a method
function
and an instance instance
. bind_method
returns a new bound method
that takes in one less argument than function
and calls function
with
instance
as the first argument. Hint: The *args
notation will be useful.
def bind_method(function, instance):
"*** YOUR CODE HERE ***"
Now we can implement the function make_instance
, which will allow us to create
new instances of the class represented by the dispatch dictionary cls
. Fill in
the get_value
and set_value
functions that get and set attributes and
methods, respectively. Make sure to think about instance vs class attributes
(which has priority?), and use the bind_method
function you just implemented.
Hint: The built-in callable
function takes in a value and returns True
if
that value is a function.
def make_instance(cls):
"""Return a new instance of the `cls` class."""
attributes = {} # instance attributes, e.g. {'a': 6, 'b': 1}
def get_value(name):
"*** YOUR CODE HERE ***"
def set_value(name, value):
"*** YOUR CODE HERE ***"
instance = {'get': get_value, 'set': set_value} # dispatch dictionary
return instance
Finally, we implement the init_instance
function, which takes in an instance
instance
and potentially some extra arguments. init_instance
gets the bound
method __init__
from instance
, if it exists, calls it, and then returns
instance
. See the make_account_class
function for an example __init__
.
def init_instance(instance, *args):
"*** YOUR CODE HERE ***"
We now have a working object system built with dictionaries and functions! This is an example of how we can transition from one paradigm to another. If everything is implemented correctly, we should now be able to use our account class as follows (check it by running the doctests):
>>> Account = make_account_class()
>>> brian_acct = Account['new']('Brian')
>>> brian_acct['get']('holder')
'Brian'
>>> brian_acct['get']('interest')
0.02
>>> brian_acct['get']('deposit')(20)
20
>>> brian_acct['get']('withdraw')(5)
15
>>> brian_acct['get']('balance')
15
>>> brian_acct['set']('interest', 0.08)
>>> Account['get']('interest')
0.02
>>> brian_acct['get']('interest')
0.08