61A Homework 6

Due by 5pm on Friday, 10/12

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

Readings. Sections 2.5, 2.6, and 2.7 of the online lecture notes.

Q1. 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 ***"

Q2. 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:

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 ***"

Q3. Write a class Amount that represents a collection of nickels and pennies. Include a property method called value that computes the total monetary value of the amount from the nickels and pennies.

Do not add an instance attribute called value to each Amount instance. Instead, an Amount should have only two instance attributes: nickels and pennies. You do not need to support direct assignment to value. (You are welcome to add that feature as well; see the relevant Python Property docs).

Finally, write a subclass MinimalAmount with base class Amount that overrides the constructor so that all amounts are minimal upon construction. An Amount instance is minimal if it has no more than four pennies, but the same value as the nickel and penny quantities passed as arguments:

class Amount(object):
    """An amount of nickels and pennies.

    >>> a = Amount(3, 7)
    >>> a.nickels
    3
    >>> a.pennies
    7
    >>> a.value
    22
    >>> a.nickels = 5
    >>> a.value
    32
    """
    "*** YOUR CODE HERE ***"

class MinimalAmount(Amount):
    """An amount of nickels and pennies that is initialized with no more than
    four pennies, by converting excess pennies to nickels.

    >>> a = MinimalAmount(3, 7)
    >>> a.nickels
    4
    >>> a.pennies
    2
    >>> a.value
    22
    """
    "*** YOUR CODE HERE ***"

Q4. Write a class Rlist that implements the recursive list data type from lecture notes Section 2.3.2, but works with Python's built-in sequence operations: the len function and subscript notation.

When len is called on an object with a user-defined class, it calls a magic method called __len__ and returns the result.

When a subscript operator is applied to an object with a user-defined class, it calls a magic method called __getitem__ with a single argument (the index) and returns the result.

As an example, the Container class below holds a single value:

class Container(object):
    """A container for a single item.

    >>> c = Container(12)
    >>> c
    Container(12)
    >>> len(c)
    1
    >>> c[0]
    12
    """

    def __init__(self, item):
        self._item = item

    def __repr__(self):
        return 'Container({0})'.format(repr(self._item))

    def __len__(self):
        return 1

    def __getitem__(self, index):
        assert index == 0, 'A container holds only one item'
        return self._item

class Rlist(object):
    """A recursive list consisting of a first element and the rest.

    >>> s = Rlist(1, Rlist(2, Rlist(3)))
    >>> len(s)
    3
    >>> s[0]
    1
    >>> s[1]
    2
    >>> s[2]
    3
    """
    "*** YOUR CODE HERE ***"

Q5. (Extra for experts) Add multiple inheritance to the object system that we implemented in class using dispatch dictionaries. You will need to make the following changes:

  1. Allow a class to be created with an arbitrary number of base classes.
  2. Classes should respond to a message 'mro' that returns the method resolution order for the class.
  3. Looking up an attribute by name in a class (using the 'get' message) should follow the method resolution order.

Choose a method resolution order from the three approaches that have been used in Python since its invention:

def make_instance(cls):
    """Return a new object instance."""
    def get_value(name):
        if name in attributes:
            return attributes[name]
        else:
            value = cls['get'](name)
            return bind_method(value, instance)

    def set_value(name, value):
        attributes[name] = value

    attributes = {}
    instance = {'get': get_value, 'set': set_value}
    return instance

def bind_method(value, instance):
    """Return value or a bound method if value is callable."""
    if callable(value):
        def method(*args):
            return value(instance, *args)
        return method
    else:
        return value

def make_class(attributes, base_classes=()):
    """Return a new class.

    attributes -- class attributes
    base_classes -- a sequence of classes
    """
    "*** YOUR CODE HERE ***"

def init_instance(cls, *args):
    """Return a new instance of cls, initialized with args."""
    instance = make_instance(cls)
    init = cls['get']('__init__')
    if init:
        init(instance, *args)
    return instance

def make_account_class():
    """Return the Account class, which has deposit and withdraw methods."""

    interest = 0.02

    def __init__(self, account_holder):
        self['set']('holder', account_holder)
        self['set']('balance', 0)

    def deposit(self, amount):
        """Increase the account balance by amount and return the new balance."""
        new_balance = self['get']('balance') + amount
        self['set']('balance', new_balance)
        return self['get']('balance')

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

    return make_class(locals())

Account = make_account_class()

def make_fee_class():
    """Return a Fee class, which has a fee class attribute."""
    fee = 1
    return make_class(locals())

Fee = make_fee_class()

def make_checking_account_class():
    """Return the CheckingAccount class, which imposes a $1 withdrawal fee.

    >>> jack_acct = CheckingAccount['new']('Jack')
    >>> jack_acct['get']('interest')
    0.01
    >>> jack_acct['get']('deposit')(20)
    20
    >>> jack_acct['get']('withdraw')(5)
    14
    """
    interest = 0.01

    def withdraw(self, amount):
        fee = self['get']('fee')
        return Account['get']('withdraw')(self, amount + fee)

    return make_class(locals(), [Account, Fee])

CheckingAccount = make_checking_account_class()