61A Homework 7

Due by 11:59pm on Wednesday, 3/13

Submission. See the online submission instructions. We have provided a starter file for the questions below.

Readings. Section 2.5 of the online lecture notes.

Q1. In lecture, we implemented a mutable rlist that supports push and pop operations. We further claimed that arbitrary list mutation, such as setitem, can be implemented in terms of push and pop. Proceed to implement the setitem behavior below and make any necessary changes to the dispatch function. Your solution should only push and pop, calling the dispatch function to do so. Do not manipulate contents directly, and do not create a new mutable rlist.

# Mutable rlist
def mutable_rlist():
    """A mutable rlist that supports push, pop, and setitem operations.

    >>> a = mutable_rlist()
    >>> a('push', 3)
    >>> a('push', 2)
    >>> a('push', 1)
    >>> a('setitem', 1, 4)
    >>> a('str')
    '<rlist (1, 4, 3)>'
    contents = empty_rlist

    def setitem(index, value):
        "*** YOUR CODE HERE ***"

    def dispatch(message, value=None):
        nonlocal contents
        if message == 'first':
            return first(contents)
        if message == 'rest':
            return rest(contents)
        if message == 'len':
            return len_rlist(contents)
        if message == 'getitem':
            return getitem_rlist(contents, value)
        if message == 'str':
            return str_rlist(contents)
        if message == 'pop':
            item = first(contents)
            contents = rest(contents)
            return item
        if message == 'push':
            contents = rlist(value, contents)

    return dispatch

def pair(x, y):
    def dispatch(m):
        if m == 0:
            return x
        elif m == 1:
            return y
    return dispatch

empty_rlist = None

def rlist(first, rest):
    return pair(first, rest)

def first(s):
    return s(0)

def rest(s):
    return s(1)

def len_rlist(s):
    if s == empty_rlist:
        return 0
    return 1 + len_rlist(rest(s))

def getitem_rlist(s, k):
    if k == 0:
        return first(s)
    return getitem_rlist(rest(s), k - 1)

def rlist_to_tuple(s):
    if s == empty_rlist:
        return ()
    return (first(s),) + rlist_to_tuple(rest(s))

def str_rlist(s):
    return '<rlist ' + str(rlist_to_tuple(s)) + '>'

Q2. Create a class called VendingMachine that represents a vending machine for some product. A VendingMachine object doesn't actually return anything but strings describing its interactions. See the doctest below for examples.

In Nanjing, there are even vending machines for crabs.

class VendingMachine(object):
    """A vending machine that vends some product for some price.

    >>> v = VendingMachine('crab', 10)
    >>> v.vend()
    'Machine is out of stock.'
    >>> v.restock(2)
    'Current crab 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 crab and $2 change.'
    >>> v.deposit(10)
    'Current balance: $10'
    >>> v.vend()
    'Here is your crab.'
    >>> v.deposit(15)
    'Machine is out of stock. Here is your $15.'
    "*** YOUR CODE HERE ***"

Q3. 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. The doctest gives an example.

Hint: Your implementation will need to use the *args notation that allows functions to take a flexible number of arguments. You may also find the hasattr function useful.

class MissManners(object):
    """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.'
    >>> 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.'
    >>> m.ask('please give up a teaspoon')
    'Thanks for asking, but I know not how to give up a teaspoon'
    >>> m.ask('please vend')
    'Here is your teaspoon and $10 change.'
    "*** YOUR CODE HERE ***"

Q4. The bank accounts we created in lecture are not particularly secure. Let's address this by creating a SecureAccount class that requires a password in order to withdraw money. Its __init__ method should take in a password string as an additional argument, and it should have a secure_withdraw method that only withdraws money if the password it is given matches the original account password. If the password doesn't match, then secure_withdraw should return 'Incorrect password'. Furthermore, if the secure_withdraw function is called three times in a row with the incorrect password, then the account should be locked; subsequent calls to secure_withdraw should return the string 'This account is locked'. (Return just the string 'Incorrect password' for the first three attempts, and just the string 'This account is locked' for later attempts.) Finally, override the withdraw method to just return the string 'This account requires a password to withdraw'.

Make sure to obey the following restrictions:

  1. Do not modify Account, but instead create a subclass SecureAccount.
  2. Do not actually modify the account balances with methods in SecureAccount. Instead, arrange that the methods of Account continue to handle that.
class Account(object):
    """A bank account that allows deposits and withdrawals.

    >>> john = Account('John')
    >>> jack = Account('Jack')
    >>> john.deposit(10)
    >>> john.deposit(5)
    >>> john.interest
    >>> jack.deposit(7)
    >>> jack.deposit(5)

    interest = 0.02

    def __init__(self, account_holder):
        self.balance = 0
        self.holder = account_holder

    def deposit(self, amount):
        """Increase the account balance by amount and return the new balance."""
        self.balance = self.balance + amount
        return self.balance

    def withdraw(self, amount):
        """Decrease the account balance by amount and return the new balance."""
        if amount > self.balance:
            return 'Insufficient funds'
        self.balance = self.balance - amount
        return self.balance

"*** YOUR CODE HERE ***"

The following code tests your implementation using the unittest module, Python's built-in unit testing framework. This module provides greater flexibility and generality than doctest. Read the online documentation to learn more about the features it provides. You can run the tests with the command python3 -m unittest -v hw7.py.

import unittest

class SecureAccountTest(unittest.TestCase):
    """Test the SecureAccount class."""

    def setUp(self):
        self.account = SecureAccount('Alyssa P. Hacker', 'p4ssw0rd')

    def test_secure(self):
        acc = self.account
        self.assertEqual(acc.balance, 1000, 'Bank error! Incorrect balance')
                         'This account requires a password to withdraw')
        self.assertEqual(acc.secure_withdraw(100, 'p4ssw0rd'), 900,
                         "Didn't withdraw 100")
        self.assertEqual(acc.secure_withdraw(100, 'h4x0r'), 'Incorrect password')
        self.assertEqual(acc.secure_withdraw(100, 'n00b'), 'Incorrect password')
        self.assertEqual(acc.secure_withdraw(100, '1337'), 'Incorrect password')
        self.assertEqual(acc.balance, 900, 'Withdrew with bad password')
        self.assertEqual(acc.secure_withdraw(100, 'p4ssw0rd'),
                         'This account is locked')
        self.assertEqual(acc.balance, 900, 'Withdrew from locked account')

Q5. (Extra for Experts) Our SecureAccount implementation still isn't quite secure, since anyone can read the password stored by an account. Modify the SecureAccount class to prevent this. Also ensure that no one can make more than three bad guesses in a row at the password (e.g. don't allow security to be bypassed by modifying a counter).

Hint: Higher-order functions may help. (You don't have to worry about inspection of closures for this exercise.)

"*** YOUR CODE HERE ***"

class MoreSecureAccountTest(SecureAccountTest):
    """Test the MoreSecureAccount class."""

    def setUp(self):
        self.account = MoreSecureAccount('Alyssa P. Hacker', 'p4ssw0rd')