CS61A Lab 8: OOP Below the Line

October 10-12, 2012

Many parts of this lab are taken from John DeNero's 61A lecture notes from Fall 2011

Today's lab will introduce you to how we could implement OOP using only functions and dictionaries, which means that Python's class and object syntax isn't necessary, but is rather just a syntactic convenience.

Note: If you're working on your own machine, instead of the lab machines, then you'll need to first copy the following file into your current working directory on your class account, and then transfer this file to your laptop/desktop:

 $ cp ~cs61a/lib/python_modules/oop.py . 
However, if you're working on a lab machine (or SSH'd into your class account), then you don't need to copy these files over.

To start off, let's cover message passing.

Message passing

Observe the following interactive session:

>>> christine = make_person("Christine", 20)
>>> christine("name")
'Christine'
>>> christine("age")
20
>>> christine("want to go out for drinks?")
"I don't understand that message."
>>> christine("well, i like your shoes")
"I don't understand that message."

christine is a function that accepts two messages, "name" and "age". Thus, we are able to pass messages to christine, and christine will respond accordingly. When we pass a message to christine that she doesn't understand, the string "I don't understand that message." is returned.

Question 1

You can copy the starter code into your current directory.

cp ~cs61a/public_html/fa12/labs/lab08/lab08.py . 

Implement a make_person function that would make the above interactive session work.

def make_person1(name, age):

The general format of a dispatch function is as follows:

def local_scope_creating_function():
    def dispatch(message):
        if message == <message1>:
           ...
        elif message == <message2>:
            ...
        ...
        else: 
            <how to handle other messages>
      return dispatch

Hopefully, your solution to number 1 looks something like:

def make_person(name, age):
    def dispatch(message):
        if message == "name":
            return name
        elif message == "age":
            return age
        else:
            return "I don't understand that message."
    return dispatch

We use the frame created by the call to make_person to create the state that the dispatch function can refer to. In this example, the dispatch function represents a person.

Question 2

Suppose we want to accept messages and a single argument. For example, we now want the following session to work:

>>> christine = make_person("Christine", 20)
>>> christine("name")
'Christine'
>>> christine("buy", "porsche")
'I just bought a porsche.'
>>> christine("inventory")
['porsche']
>>> christine("change name", "Steven")
>>> christine("name")
'Steven'

In the lab08.py file, complete the make_person2 function such that the above interactive session works. Notice that christine now takes an optional additional argument: you can implement this by creating another parameter that has a default value, making it optional.

def make_person2(name, age):

christine now functions quite similarly to an object. Hopefully you used nonlocal to allow for the dispatch function to accept the message "change name". By using nonlocal and the dispatch function, we have successfully created persistent state. This is starting to look like an instance of a class. However, we're still missing a few important pieces, such as class variables.

To summarize, we just used the idea of message passing to create persistent state along with other behaviors, such as changing names or buying things. The dispatch function is a function that has some local state (created by the function that encompasses it). The idea of message passing is to organize computation by passing "messages" to each of these dispatch functions. The messages are strings that correspond to particular behaviors, which, if you think about it, is quite similar to how Python OOP functionality works.

Dispatch Dictionaries

Let's look at a new variation of message passing. We're going to create something called a dispatch dictionary. A dispatch dictionary is a Python dictionary whose keys are considered as messages, and whose values are functions that correspond to the messages.

Take a few minutes to read over and understand the following code:

def make_person(name, age):
    attributes = {'name': name, 'age': age, 'inventory': []}
    def get_name():
        return attributes['name']
    def get_age():
        return attributes['age']
    def get_inventory():
        return attributes['inventory']
    person = {'name': get_name, 
              'age': get_age, 
              'inventory': get_inventory,
             }
    return person

Instead of returning a dispatch function, this version of make_person returns a dispatch dictionary. In essence, dispatch functions and dispatch dictionaries both respond to messages being passed to them. In this case, the person variable is the dispatch dictionary, and it responds to the messages 'name', 'age', and 'inventory' by returning a corresponding function.

State is created by using another dictionary, called attributes in this example.

Question 3.0

(Not really a question, but you should stop and think here.)

Take a look at the following interactive session, and verify in your head how each line works:

>>> christine = make_person("Christine", 20)
>>> christine["name"]()
'Christine'
>>> blah = christine["age"]
>>> blah()
20
>>> christine["inventory"]()
[]

Notice how christine is now a dictionary rather than a function, and when we look up a value in christine, a function is returned, which we have to call.

Continuing the interactive session:

>>> christine["buy"]("porsche")
'I just bought a porsche.'
>>> christine["inventory"]()
['porsche']
>>> christine["change name"]("Steven")
>>> christine["name"]()
'Steven'

Question 3

Modify the dispatch dictionary version make_person3 so that Christine accepts the messages "buy" and "change_name", and implement the functionality as per the previous interactive session.

def make_person3(name, age):

Object Oriented Programming

The rest of this lab covers below the line OOP. We refer to functions in oop.py, which you can view here or copy to your current directory by the method outlined at the beginning. If there's a piece of code that's confusing you, don't hesitate to ask your TA for help!

The Python object system uses special syntax such as the class statement and the use of dot notation. We will soon see, however, that it is possible to implement classes and objects using only functions and dictionaries.

In order to implement objects, we will abandon dot notation (which does require built-in language support) and create dispatch dictionaries that behave in much of the same way as the elements of the built-in object system. We have already seen how to implement message-passing behavior through dispatch dictionaries. To implement an object system in full, we send messages between instances, classes, and base classes, all of which are dictionaries that contain attributes.

Instances

If you think about it, there are really only two things you can do with an object: get values and set values. Thus, we will represent an instance as a dispatch dictionary that accepts only two messages: 'get' and 'set'.

Take a few moments to read over the following code.

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 a bound method if value is callable, or value otherwise."""
    if callable(value):
        def method(*args):
            return value(instance, *args)
        return method
    else:
        return value

That seems like a lot to take in! Remember, a bound method is simply a function that is "bound" to an object, meaning that when the method is called, the object is automatically passed in as the first argument self. That's what the function bind_method is doing.

Note: callable is a built-in function that, given an argument thing, returns True if and only if thing is a function object (i.e. can be called).

Let's take a closer look at get_value.

    def get_value(name):
        if name in attributes:
            return attributes[name]
        else:
            value = cls['get'](name)
            return bind_method(value, instance)

From this definition, we can infer that the attributes dictionary only contains instance variables, meaning it does not include bound methods. Methods are stored in the class, and thus we must 'get' the value of the method from the class. Note that we might also be getting a class variable instead, so bind_method does a check to see if the value is callable or not and reacts accordingly.

Let's look at classes:

Classes

def make_class(attributes, base_class=None):
    """Return a new class, which is a dispatch dictionary."""
    def get_value(name):
        if name in attributes:
            return attributes[name]
        elif base_class is not None:
            return base_class['get'](name)
    def set_value(name, value):
        attributes[name] = value
    def new(*args):
        return init_instance(cls, *args)
    cls = {'get': get_value, 'set': set_value, 'new': new}
    return cls

Unlike an instance, the get function for classes does not query its class when an attribute is not found, but instead queries its base_class. No method binding is required for classes.

Notice that make_class takes in two parameters, attributes and base_class. The parameter attributes is a dispatch dictionary of methods and class variables, and base_class would refer to a parent class (if specified).

Thus, to create a class, you would need to call make_class with a dispatch dictionary of attributes.

Initialization. The new function in make_class calls init_instance, which first makes a new instance, then invokes a method called __init__.

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

This final function completes our object system. We now have instances, which set locally, but fall back to their classes on get. After an instance looks up a name in its class, it binds itself to function values to create methods. Finally, classes can create new instances, and they apply their __init__ constructor function immediately after instance creation.

In this object system, the only function that should be called by the user is make_class. All other functionality is enabled through message passing. Similarly, Python's object system is invoked via the class statement, and all of its other functionality is enabled through dot expressions and calls to classes.

Question 4

Let's use our below the line implementation of OOP to recreate the VendingMachine class from the hoemwork. In your lab08.py file, fill in the parts that say *YOUR CODE HERE*. An example interactive session can be found below the code, and this session should help guide your code.

A Vending machine should have the following attributes:

  • 'name': The item we want to vend
  • 'price': Cost per item
  • 'stock': The quatity of product we have in stock
  • 'balance': How much money the user has deposited so far
  • 'vend': Method to request something from the machine
  • 'restock': Method to add stock to the vending machine
  • 'deposit': Method to deposit money in the vending machine.

Fill in the missing parts in your lab08.py file.

from oop import *
def make_vending_machine_class():
    def __init__(self, name, price):
        self['set']('name', name)
        "***FINISH INIT DEFINITION HERE***"
        
    def vend(self):
        if self['get']('stock') <= 0:
            return "Machine is out of stock."
        change = self['get']('balance') - self['get']('price')
        if change < 0:
            return "You must deposit $" + str(self['get']('price') - self['get']('balance')) + " more."
        self['set']('stock', self['get']('stock') - 1)
        self['set']('balance', 0)
        result = "Here is your " + self['get']('name')
        if change:
            result += " and $" + str(change) + " change"
        return result + "."
        
    def restock(self, amount):
        """***YOUR CODE HERE***"""
        
    def deposit(self, amount):
        """***YOUR CODE HERE***"""

    # Finish the return statement below.
    return make_class("""***YOUR CODE HERE***""")

Interactive session:

    >>> VendingMachine = make_vending_machine_class()
    >>> v = VendingMachine['new']('crab', 10)
    >>> v['get']('vend')()
    'Machine is out of stock.'
    >>> v['get']('restock')(2)
    'Current crab stock: 2'
    >>> v['get']('vend')()
    'You must deposit $10 more.'
    >>> v['get']('deposit')(7)
    'Current balance: $7'
    >>> v['get']('vend')()
    'You must deposit $3 more.'
    >>> v['get']('deposit')(5)
    'Current balance: $12'
    >>> v['get']('vend')()
    'Here is your crab and $2 change.'
    >>> v['get']('deposit')(10)
    'Current balance: $10'
    >>> v['get']('vend')()
    'Here is your crab.'
    >>> v['get']('deposit')(15)
    'Machine is out of stock. Here is your $15.'

To verify your solution works, be sure to type in the code from the interactive session or run the doctests to see if you get the same results.

Question 5

Finally, let's create a child class using our below the line implementation.

Define a function make_taxed_vending_machine_class that makes a class that inherits from the Vending Machine class from Question 4. There is one difference:

  1. The taxed vending machine should charge a 5% tax on everything.
def make_taxed_vending_machine_class():

You should not re-write any unnecessary code.

Note that the vending machine should require a price that is 5% greater than what is passed to the constructor. You should only need to rewrite the constructor.

If you finished this lab, CONGRATULATIONS! You just implemented an object oriented programming system using only functions and dictionaries!