CS61A Homework 8

Due by 11:59 PM on (that is, the end of) Tuesday, 7/17

This homework must be submitted online. We do not require a paper copy. If you would like a reader to give you written feedback on your homework, submit a paper copy to the homework box.

To turn in the electronic copy, submit all of your answers in a file named hw8.py. Follow the instructions here to submit the electronic copy.

If you would like, you can use the template file hw8.py. To copy this file to your lab account you can run the command:

      cp ~cs61a/lib/hw/hw08/hw8.py .
      

to copy it into your current directory.

In homeworks, we have three different categories of questions:

It is our hope that these categories will help guide you in deciding how to divide your time when working on the homeworks.

Core Questions

Q1. Write a class Amount that represents a collection of nickels and pennies.

Include a property that computes the value of the amount from the nickels and pennies. Do not add a value attribute to each Amount instance.

Finally, write a subclass MinimalAmount with base class Amount that overrides the constructor so that all amounts are minimal. An Amount is minimal if it has no more than four pennies, so that it uses the minimal number of coins necessary to represent the amount.

An example usage of the two Amount classes is shown in the doctests for the classes below and in the template file:

      class Amount(object):
          """An amount of nickels and pennies.
          
          >>> a = Amount(3, 7)
          >>> a.nickels
          3
          >>> a.pennies
          7
          >>> a.value
          22
          >>> a.pennies += 6
          >>> a.pennies
          13
          >>> a.value
          28
          """
          
      class MinimalAmount(Amount):
          """An amount of nickels and pennies with
          no more than four pennies.
          
          >>> a = MinimalAmount(3, 7)
          >>> a.nickels
          4
          >>> a.pennies
          2
          >>> a.value
          22
          >>> a.pennies += 6
          >>> a.pennies # Doesn't stay minimal!
          8
          >>> a.value
          28
          """
      

Q2. For the rest of this homework, we will concern ourselves with implementing classes that represent banks and the accounts held by customers of the bank. We have provided starter code for these classes in the template file.

We would like to maintain information about the total holdings of all balances of any bank account. One way we could do this is to make a total_holdings class variable inside the Account class, or a class attribute __total_holdings and a method total_holdings(). While this is fine as an example to explain a Python feature, it is not particularly good design. For one thing, it assumes that there is only one bank. It would be better to create another class to represent banks. Implement such a class, Bank, that eliminates all use of class attributes inside the Account class.

Accounts will behave similar to before, except that the total_holdings method applies only to banks, and the constructor for the Account class is never called directly by clients. We want the following behavior:

      >>> third_national = Bank()
      >>> second_federal = Bank()
      >>> acct0 = third_national.make_account(1000)
      >>> acct0.withdraw(100)
      >>> acct1 = third_national.make_account(2000)
      >>> third_national.total_holdings()
      2900
      >>> second_federal.total_holdings()
      0
      >>> acct1.deposit(300)
      >>> acct1.balance()
      2300
      >>> third_national.total_holdings()
      3200
      >>> acct1.total_holdings()
      Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      AttributeError: 'Account' object has no attribute 'total_holdings'
      >>> acct1.bank().total_holdings()
      3200
      

Hint: A bank will need to be able to look up all of its accounts to find out what is currently its total holdings. This means each bank needs to store all of its accounts for later use. It might be helpful to then give each bank an instance variable that holds a list of all its accounts. The trick is that this list should be updated each time a bank opens a new account.

Hint: An account needs to know the bank it belongs to for the bank() method to work. Make the bank for the account an argument to the Account constructor.

Q3. Our bank accounts are not particularly secure. Suppose our Bank class wishes to provide additional security for account holders who want it (and presumably pay for it). Extend your solution to the previous question so that aBank.make_secure_account(1000, "The magic woid") returns an account whose deposit, withdraw, and balance operations all take an extra, trailing argument that must match the passphrase "The magic woid" for the operation to work. This should be done in a particular way:

  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.

The desired behavior is as follows:

      >>> third_national = Bank()
      >>> acct3 = third_national.make_secure_account(1000, "The magic woid")
      >>> acct3.deposit(1000, 'The magic woid')
      >>> acct3.balance('The magic woid')
      2000
      >>> acct3.balance('Foobar')
      Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      SecurityError: wrong passphrase or account
      >>> acct3.balance()
      Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      SecurityError: passphrase missing
      

Note: You may have noticed that we are using a SecurityError in this question, which is a subclass of the BaseException class. This is one of our first looks at an exception or error class in Python. You can learn more about exceptions by reading the Python Errors and Exceptions Tutorial. For this question, we are particularly concerned with instantiating errors and raising errors. To create an error, we construct it much like we would any other object:

      >>> my_error = BaseException("This is my error's message!")
      

Notice that the argument to the constructor was the error message that we would like to associate with the error.

Now that we know how to make errors, we can now raise the error, indicating that a problem occurred. The syntax for raising an error is:

      >>> raise BaseException("You raise me up!")
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
      BaseException: You raise me up!
      

Notice that the error message that we constructed the error with will appear after the type of error that was raised.

Q4. 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 was constructed with, but only if the caller said please. The doctest gives an example.

To complete this question, it will be helpful to know about the getattr and hasattr built-in functions. We use these functions in our solution.

Hint: Your implementation will need to use the *args notation that allows functions to take any number of variables. You might also find the string function split useful here.

      class MissManners(object):
          """A container class that only forward messages that say please.

          >>> some_bank = Bank()
          >>> a = some_bank.make_account(100)
          >>> m = MissManners(a)
          >>> m.ask('balance')
          'You must learn to say please.'
          >>> m.ask('please balance')
          100
          >>> m.ask('please deposit', 20)
          >>> m.ask('balance')
          'You must learn to say please.'
          >>> m.ask('please show me the balance')
          'Thanks for asking, but I know not how to show me the balance'
          >>> m.ask('please balance')
          120
          """
      

Reinforcement Questions

Q5. Python's object-oriented features are very dynamic compared to those of languages like Java or C++. As one example, in Python, when an attribute reference obj.attr fails because attr is not defined in obj, Python tries evaluating obj.__getattr__("attr") to get the value of the attribute, if obj has the method __getattr__ defined. Then, if this method can't make sense of the request either, it is supposed to raise the exception AttributeError, and otherwise return some reasonable value in place of obj.attr. Documentation regarding the __getattr__ method can be found here.

Define a class Monitor that acts as a wrapper around objects that counts the number of times each attribute of that object is evaluated. A "monitored object" will behave just like the object itself, but as a side effect will maintain a dictionary that maps method names to counts of the number of times that method is called. We want the following behavior as an example, although your solution should work for any type of object, not just for Accounts:

      >>> B = Bank()
      >>> acct = B.make_account(1000)
      >>> mon_acct = Monitor(acct)
      >>> mon_acct.balance()
      1000
      >>> for i in range(10): mon_acct.deposit(100)
      >>> mon_acct.withdraw(20)
      >>> mon_acct.balance()
      1980
      >>> mon_acct.access_count('balance')
      2
      >>> mon_acct.access_count('deposit')
      10
      >>> mon_acct.access_count('withdraw')
      1
      >>> mon_acct.access_count('clear')
      0
      >>> L = list(mon_acct.attributes_accessed())
      >>> L.sort()
      >>> L
      ['balance', 'deposit', 'withdraw']
      

Hint: You may find your solution to ask for question 4 helpful here. Using __getattr__ in this solution is very similar.

Extra for Experts

Q6. Another kind of wrapper class you might imagine to be useful is one that records all the arguments that are passed to a certain object's methods. Write a class ArgumentMonitor that wraps around any object and keeps track of a set of all combinations of arguments passed to a function, with the number of times each combination of arguments is used. For example:

      >>> B = Bank()
      >>> acct = B.make_account(1000)
      >>> mon_acct = ArgumentMonitor(acct, ['balance', 'withdraw', 'deposit'])
      >>> mon_acct.balance()
      1000
      >>> for i in range(10): mon_acct.deposit(100)
      >>> mon_acct.withdraw(20)
      >>> mon_acct.withdraw(10)
      >>> mon_acct.balance()
      1970
      >>> d = mon_acct.argument_counts('balance')
      >>> d[()]
      2
      >>> d = mon_acct.argument_counts('deposit')
      >>> list(d.items())
      [((100,), 10)]
      >>> d = mon_acct.argument_counts['withdraw']
      >>> d[(10,)]
      1
      >>> d[(20,)]
      1