Object Oriented Programming

What is it?

Now we move onto what might have been termed an advanced topic up until about 5 years ago. Nowadays 'Object Oriented Programming has become the norm. Languages like Java and Python embody the concept so much that you can do very little without coming across objects somewhere. So what's it all about?

The best introductions are, in my opinion:

These increase in depth, size and academic exactitude as you go down the list. For most non professional programmers' purposes the first is adequate. For a more programming focussed intro try Object Oriented Programming by Timothy Budd(2nd edition). I haven't personally read this one but it gets rave reviews from people whose opinions I respect. Finally for a whole heap of info on all topics OO try the Web link site at: http://www.cetus-links.org

Assuming you don't have the time nor inclination to research all these books and links right now, I'll give you a brief overview of the concept. (Note:Some people find OO hard to grasp others 'get it' right away. Don't worry if you come under the former category, you can still use objects even without really 'seeing the light'.)

One final point, we will only be using Python in this section since neither BASIC not Tcl support objects. It is possible to implement an Object Oriented design in a non OO language through coding conventions, but it's usually an option of last resort rather than a recommended strategy. If your problem fits well with OO techniques then it's best to use an OO language.

Data and Function - together

Objects are collections of data and functions that operate on that data. These are bound together so that you can pass an object from one part of your program and they automatically get access to not only the data attributes but the operations that are available too.

For example a string object would store the character string but also provide methods to operate on that string - search, change case, calculate length etc.

Objects use a message passing metaphor whereby one object passes a message to another object and the receiving object responds by executing one of its operations, a method. So a method is invoked on receipt of the corresponding message by the owning object. There are various notations used to represent this but the most common mimics the access to fields in records - a period. Thus, for a fictitious widget class:

w = Widget() # create new instance, w, of widget
w.paint()  # send the message 'paint' to it

This would cause the paint method of the widget object to be invoked.

Defining Classes

Just as data has various types so objects can have different types. These collections of objects with identical characteristics are collectively known as a class. We can define classes and create instances of them, which are the actual objects. We can store references to these objects in variables in our programs.

Let's look at a concrete example to see if we can explain it better. We will create a message class that contains a string - the message text - and a method to print the message.

class Message:
    def __init__(self, aString):
        self.text = aString
    def printIt(self):
        print self.text

Note 1:One of the methods of this class is called __init__ and it is a special method called a constructor. The reason for the name is that it is called when a new object instance is created or constructed. Any variables assigned (and hence created in Python) inside this method will be unique to the new instance. There are a number of special methods like this in Python, nearly all distinguished by the __xxx__ naming format.

Note 2:Both the methods defined have a first parameter self. The name is a convention but it indicates the object instance. As we will see this parameter is filled in by the interpreter at run-time, not by the programmer. Thus print is called, on an instance of the class (see below), with no arguments: m.printIt().

Note 3:We called the class Message with a capital 'M'. This is purely convention, but it is fairly widely used, not just in Python but in other OO languages too. A related convention says that method names should begin with a lowercase letter and subsequent words in the name begin with uppercase letters. Thus a method called "calculate current balance" would be written: calculateCurrentBalance.

You may want to briefly revisit the 'Data' section and look again at 'user defined types'. The Python address example should be a little clearer now. Essentially the only type of used defined type in Python is a class. A class with attributes but no methods (except __init__ is effectively equivalent to a BASIC record.

Using Classes

Having defined a class we can now create instances of our Message class and manipulate them:

m1 = Message("Hello world")
m2 = Message("So long, it was short but sweet")

note = [m1, m2] # put the objects in a list
for msg in note:
    msg.printIt() # print each message in turn

So in essence you just treat the class as if it was a standard Python data type, which was after all the purpose of the excercise!

Same thing, Different thing

What we have so far is the ability to define our own types (classes) and create instances of these and assign them to variables. We can then pass messages to these objects which trigger the methods we have defined. But there's one last element to this OO stuff, and in many ways it's the most important aspect of all.

If we have two objects of different classes but which support the same set of messages but with their own corresponding methods then we can collect these objects together and treat them identically in our program but the objects will behave differently. This ability to behave differently to the same input messages is known as polymorphism.

Typically this could be used to get a number of different graphics objects to draw themselves on receipt of a 'paint' message. A circle draws a very different shape from a triangle but provided they both have a paint method we, as programmers, can ignore the difference and just think of them as 'shapes'.

Let's look at an example, where instead of drawing shapes we calculate their areas:

First we create Square and Circle classes:

class Square:
    def __init__(self, side):
        self.side = side
    def calculateArea(self):
        return self.side**2

class Circle:
    def __init__(self, radius):
        self.radius = radius
    def calculateArea(self):
        import math
        return math.pi*(self.radius**2)

Now we can create a list of shapes (either circles or squares) and then print out their areas:

list = [Circle(5),Circle(7),Square(9),Circle(3),Square(12)]

for shape in list:
    print "The area is: ", shape.calculateArea()

Now if we combine these ideas with modules we get a very powerful mechanism for reusing code. Put the class definitions in a module - say 'shapes.py' and then simply import that module when we want to manipulate shapes. This is exactly what has been done with many of the standard Python modules, which is why accessing methods of an object looks a lot like using functions in a module.

Inheritance

Inheritance is often used as a mechanism to implement polymorphism. Indeed in many OO languages it is the only way to implement polymorphism. It works as follows:

A class can inherit both attributes and operations from a parent or super class. This means that a new class which is identical to another class in most respects does not need to reimplement all the methods of the existing class, rather it can inherit those capabilities and then override those that it wants to do differently (like the paint method in the case above)

Again an example might illustrate this best. We will use a class heirarchy of bank accounts where we can deposit cash, obtain the balance and make a withdrawal. Some of the accounts provide interest (which, for our purposes, we'll assume is calculated on every deposit - an interesting innovation to the banking world!) and others charge fees for withdrawals.

The BankAccount class

Let's see how that might look. First let's consider the attributes and operations of a bank account at the most general (or abstract) level.

Its usually best to consider the operations first then provide attributes as needed to support these operations. So for a bank account we can:

  • Deposit cash,
  • Withdraw cash,
  • Check current balance and
  • Transfer funds to another account.

    To support these operations we will need a bank account ID(for the transfer operation) and the current balance.

    We can create a class to support that:

    class BalanceError(Exception):
          value = "Sorry you only have $%6.2f in your account"
    
    class BankAccount:
        def __init__(self, initialAmount):
           self.balance = initialAmount
           print "Account created with balance %5.2f" % self.balance
    
        def deposit(self, amount):
           self.balance = self.balance + amount
    
        def withdraw(self, amount):
           if self.balance >= amount:
              self.balance = self.balance - amount
           else:
              BalanceError.value = BalanceError.value % self.balance
              raise BalanceError
    
        def checkBalance(self):
           return self.balance
           
        def transfer(self, amount, account):
           try: 
              self.withdraw(amount)
              account.deposit(amount)
           except BalanceError:
              print BalanceError.value
    

    Note 1: We check the balance before withdrawing and also the use of exceptions to handle errors. Of course there is no error type BalanceError so we needed to create one - it's simply an instance of the Exception class with a string value. When we raise it we pass the original argument augmented by the current balance. Notice that we didn't use self when defining the value, that's because value is a shared attribute across all instances, it is defined at the class level and known as a class variable. We access it by using the class name followed by a dot: BalanceError.value as seen above.

    Note 2: The transfer method uses the BankAccount's withdraw/deposit member functions or methods to do the transfer. This is very common in OO and is known as self messaging. It means that derived classes can implement their own versions of deposit/withdraw but the transfer method can remain the same for all account types.

    The InterestAccount class

    Now we use inheritance to provide an account that adds interest (we'll assume 3%) on every deposit. It will be identical to the standard BankAccount class except for the deposit method. So we simply overrride that:

    class InterestAccount(BankAccount):
       def deposit(self, amount):
           BankAccount.deposit(self,amount)
           self.balance = self.balance * 1.03
           
    

    And that's it. We begin to see the power of OOP, all the other methods have been inherited from BankAccount (by putting BankAccount inside the parentheses after the new class name). Notice also that deposit called the superclass's deposit method rather than copying the code. Now if we modify the BankAccount deposit to include some kind of error checking the sub-class will gain those changes automatically.

    The ChargingAccount class

    This account is again identical to a standard BankAccount class except that this time it charges $3 for every withdrawal. As for the InterestAccount we can create a class inheriting from BankAccount and modifying the withdraw method.

    class ChargingAccount(BankAccount):
        def __init__(self, initialAmount):
            BankAccount.__init__(self, initialAmount)
            self.fee = 3
            
        def withdraw(self, amount):
            BankAccount.withdraw(self, amount+self.fee)
    

    Note 1: We store the fee as an instance variable so that we can change it later if necessary. Notice that we can call the inherited __init__ just like any other method.

    Note 2: We simply add the fee to the requested withdrawal and call the BankAccount withdraw method to do the real work.

    Note 3: We introduce a side effect here in that a charge is automatically levied on transfers too, but that's probably what we want, so is OK.

    Testing our system

    To check that it all works try executing the following piece of code (either at the Python prompt or by creating a separate test file).

    from bankaccount import *
    
    # First a standard BankAccount
    a = BankAccount(500)
    b = BankAccount(200)
    a.withdraw(100)
    # a.withdraw(1000)
    a.transfer(100,b)
    print "A = ", a.checkBalance()
    print "B = ", b.checkBalance()
    
    
    # Now an InterestAccount
    c = InterestAccount(1000)
    c.deposit(100)
    print "C = ", c.checkBalance()
    
    
    # Then a ChargingAccount
    d = ChargingAccount(300)
    d.deposit(200)
    print "D = ", d.checkBalance()
    d.withdraw(50)
    print "D = ", d.checkBalance()
    d.transfer(100,a)
    print "A = ", a.checkBalance()
    print "D = ", d.checkBalance()
    
    
    # Finally transer from charging account to the interest one
    # The charging one should charge and the interest one add
    # interest
    print "C = ", c.checkBalance()
    print "D = ", d.checkBalance()
    d.transfer(20,c)
    print "C = ", c.checkBalance()
    print "D = ", d.checkBalance()
    

    Now uncomment the line a.withdraw(1000) to see the exception at work.

    That's it. A reasonably straightforward example but it shows how inheritance can be used to quickly extend a basic framework with powerful new features.

    We've seen how we can build up the example in stages and how we can put together a test program to check it works. Our tests were not complete in that we didn't cover every case and there are more checks we could have included - like what to do if an account is created with a negative amount...

    Collections of Objects

    One problem that might have occured to you is how we deal with lots of objects. Or how to manage objects which we create at runtime. Its all very well creating Bank Accounts statically as we did above:

    acc1 = BankAccount(...)
    acc2 = BankAccount(...)
    acc3 = BankAccount(...)
    etc...
    

    But in the real world we don't know in advance how many accounts we need to create. How do we deal with this? Lets consider the problem in more detail:

    We need some kind of 'database' that allows us to find a given bank account by its owners name (or more likely their bank account number - since one person can have many accounts and several persons can have the same name...)

    Finding something in a collection given a unique key....hmmm, sounds like a dictionary! Lets see how we'd use a Python dictionary to hold dynamically created objects:

    from bankaccount import *
    import time
    
    # Create new function to generate unique id numbers
    def getNextID():
        ok = raw_input("Create account[y/n]? ")
        if ok[0] in 'yY':  # check valid input
           id = time.time() # use current time as basis of ID
           id = int(id) % 10000 # convert to int and shorten to 4 digits
        else: id = -1  # which will stop the loop
        return id
        
    # Let's create some accounts and store them in a dictionary
    accountData = {}  # new dictionary
    while 1:          # loop forever
       id = getNextID()
       if id == -1: 
          break       # break forces an exit from the while loop
       bal = float(raw_input("Opening Balance? "))  # convert string to float  
       accountData[id] = BankAccount(bal) # use id to create new dictionary entry
       print "New account created, Number: %04d, Balance %0.2f" % (id,bal)
    
    # Now lets access the accounts
    for id in accountData.keys():
        print "%04d\t%0.2f" % (id,accountData[id].checkBalance())
    
    # and find a particular one
    # Enter non number to force exception and end program
    while 1:
       id = int(raw_input("Which account number? "))
       if id in accountData.keys():
          print "Balance = %0.2d" % accountData[id].checkBalance()
       else: print "Invalid ID"
    

    Of course the key you use for the dictionary can be anything that uniquely identifies the object, it could be one of its attributes, like name say. Anything at all that is unique. You might find it worthwhile going back to the raw materials chapter and reading the dictionary section again, they really are very useful containers.

    Saving Your Objects

    One snag with all of this is that you lose your data when the program ends. You need some way of saving objects too. As you get more advanced you will learn how to use databases to do that but we will look at using a simple text file to save and retrieve objects. (If you are using Python there are a couple of modules called Pickle and Shelve) that do this much more effectively but as usual I'll try to show you the generic way to do it that will work in any language. Incidentally the technical term for the ability to save and restore objects is Persistence.

    The generic way is do this is to create save and restore methods at the highest level object and override in each class, such that they call the inherited version and then add their locally defined attributes:

    class A:
       def __init__(self,x,y):
         self.x = x
         self.y = y
    
       def save(self,fn):
         f = open(fn,"w")
         f.write(str(self.x)+ '\n') # convert to a string and add newline
         f.write(str(self.y)+'\n')
         return f             # for child objects to use
    
       def restore(self, fn):
         f = open(fn)
         self.x = int(f.readline()) # convert back to original type
         self.y = int(f.readline())
         return f
         
    class B(A):
       def __init__(self,x,y,z):
         A.__init__(self,x,y)
         self.z = z
       
       def save(self,fn):
         f = A.save(self,fn)  # call parent save
         f.write(str(self.z)+'\n')
         return f         # in case further children exist
       
       def restore(self, fn):
         f = A.restore(self,fn)
         self.z = int(f.readline())
         return f
    
    # create instances
    a = A(1,2)
    b = B(3,4,5)
    
    # save the instances
    a.save('a.txt').close() # remember to close the file
    b.save('b.txt').close()
    
    # retrieve instances
    newA = A(5,6)
    newA.restore('a.txt').close() # remember to close the file
    newB = B(7,8,9)
    newB.restore('b.txt').close()
    print "A: ",newA.x,newA.y
    print "B: ",newB.x,newB.y,newB.z
    
    Note: The values printed out are the restored values not the ones we used to create the instances.

    The key thing is to override the save/restore methods in each class and to call the parent method as the first step. Then in the child class only deal with child class attributes. Obviously how you turn an attribute into a string and save it is up to you the programmer but it must be output on a single line. When restoring you simply reverse the storing process.

    Hopefully this has given you a taste of Object Oriented Programming and you can move on to some of the other online tutorials, or read one of the books mentioned at the beginning for more information and examples.


    Previous  Next  Contents


    If you have any questions or feedback on this page send me mail at: alan.gauld@btinternet.com