next up previous contents
Next: Inheritance Up: Writing Modules Previous: Private Attributes   Contents

A First Example of Classes

To clarify these ideas, we'll consider a simple example - an ``interactive'' database of phone numbers and email addresses. To get started with designing a class, it's often easiest to think about the information you're going to manipulate when you eventually operate on that class. You don't need to think of everything the first time you create the class; one of the many attractive features of working with classes is that you can easily modify them as you get deeper into solving a problem. In the case of our database, the basic information we'll be storing will be a name, an phone number and an email address. We'll call the class that will hold this information Friend, and when we actually store our information it will be in what is generically known as an object; in this case a Friend object. We can define methods by indenting their definitions under the class statement.

When we create a Friend object, all we need to do is store the name, phone number and email address of a friend, so the __init__ method will be very simple - we just need to create three attributes which we'll call name, phone and email. This object will serve as a building block for our database, so we don't need to define many other methods. As an example, we'll create a __str__ method, and an identical __repr__ method so that we can display information about our friends in a readable format.

>>> class Friend:
...     def __init__(self,name,phone='',email=''):
...             self.name = name
...             self.phone = phone
...             self.email = email
...     def __str__(self):
...             return 'Name: %s\nPhone: %s\nEmail: %s' % \
                                (self.name,self.phone,self.email) 
...     def __repr__(self):
...             return self.__str__()
... 
>>> x = Friend('Joe Smith','555-1234','joe.smith@notmail.com')
>>> y = Friend('Harry Jones','515-2995','harry@who.net')
>>> print x
Name: Joe Smith
Phone: 555-1234
Email: joe.smith@notmail.com
>>> y
Name: Harry Jones
Phone: 515-2995
Email: harry@who.net
Just as with functions, we can provide default values for arguments passed to methods. In this case, name is the only required argument; the other two arguments default to empty strings. Notice that attributes are refered to as self.name; without the leading self, variables will be treated as local, and will not be stored with the object. Finally, notice that the print operator as well as the interactive session's default display have been overridden with the __str__ and __repr__ methods, respectively.

Now, we can create a database containing information about our friends, through the Frienddb class. To make it useful, we'll introduce persistence through the cPickle module introduced in Section 8.10.1. When we first create the database, we'll require the name of a file. If the file exists, we'll assume it's a database we've already created and work with that; if the file doesn't exist, we'll open it and prepare it to receive our pickled database. For this simple application, we can simply create a list of Friend objects, and store the list as an attribute in our Frienddb object.

When designing an object oriented application like this, we simply need to identify the operations we'll want to do on the object we've created. In addition to the __init__ method, some of the tasks we'd want to implement would be adding a new friend to the database, searching the database for information about a particular person, and storing the database when we're done with it. (In the following sections, it's assumed that the class definition for Friend is either in the same file as the Frienddb definition or has been properly imported along with the other appropriate modules (os,cPickle, sys,re and string). In addition, the following class statement will have appeared before the definition of these methods:

class Frienddb:

The __init__ method for our database will take care of all the dealings with the file in which the database will be stored.

import sys
class Frienddb:
    def __init__(self,file=None):
        if file == None:
            print 'Must provide a filename'
            return 
        self.file = file
        if os.path.isfile(file):
            try:
                f = open(file,'r')
            except IOError:
                sys.stderr.write('Problem opening file %s\n' % file)
                return
            try:
                self.db = cPickle.load(f)
                return
            except cPickle.UnpicklingError:
                print >>sys.stderr, '%s is not a pickled database.' % file
                return
            f.close()
        else:
            self.db = []
Since the __init__ statement acts as a constructor for class objects, it should never return a value. Thus, when errors are encountered, a return statement with no value must be used. This method puts two attributes into our Frienddb object: file, the name of the file to store the database, and db, the list containing Friend objects (if the database already exists) or an empty list if there is not a file with the name passed to the method.

The method for adding an entry to the database is simple:

    def add(self,name,phone='',email=''):
        self.db.append(Friend(name,phone,email))

Searching the database is a little more complex. We'll use the re module (Section 8.5) to allow regular expression searches, and return a list of Friend objects whose name attributes match the pattern which is provided. Since we'll be searching each name in the database, the regular expression is compiled before being used.

    def search(self,pattern):
        srch = re.compile(pattern,re.I)
        found = []
        for item in self.db:
            if srch.search(item.name):
                found.append(item)
        return found
Finally, we'll write methods to store the contents of the database into the file that was specified when the database was created. This is just a straightforward use of the cPickle module (Section 8.10.1). To insure that data is not lost, a __del__ method which simply calls the store method (provided that there were entries in the database) will be added as well:
    def store(self):
        try:
            f = open(self.file,'w')
        except IOError:
            print >>sys.stderr, \
                    'Problem opening file %s for write\n' % self.file
            return
        p = cPickle.Pickler(f,1)
        p.dump(self.db)
        f.close()
    
    def __del__(self):
        if self.db:
            self.store()

Having created these two classes, we can now write a simple program to use them. You might want to include such a program in the file containing the class definitions; if so, see Section 10.3. We'll write a small interactive program that will recognize three commands: ``a'', which will cause the program to prompt for a name, phone number and email address and the add it to the database, ``? pattern'', which will print all the records whose name attribute matches pattern, and q, which will store the database and exit. In the program that follows, I'm assuming that the class definitions for Friend and Frienddb are stored in a file called frienddb.py which is in a directory on the python search path.

import frienddb
file = raw_input('File? ')
fdb = Frienddb(file)
    
while 1:
    line = raw_input('> ')
    if line[0] == 'a':
        name = raw_input('Name? ')
        phone = raw_input('Phone number? ')
        email = raw_input('Email? ')
        fdb.add(name,phone,email)
    elif line[0] == '?':
        line = string.strip(line[1:])
        fdb.search(line)
    elif line[0] == 'q':
        fdb.store()
        break

Other methods could be implemented as well. While not necessarily that useful, suppose we wanted to access elements of our database either by a numeric index, indicating their position in the database, or by the name attribute of the individual records. Using the __getitem__ method, we could subscript the database just as we do lists or dictionaries. To properly process both string and numeric indices, we'd need to distinguish between these two types of values. One way to do this is by trying an numeric conversion of the index with the int function; if an exception is thrown, then we must have a string value.

    def __getitem__(self,key):
        try:
            index = int(key)
            return self.db[index]
        except ValueError:
            found = 0
            for item in self.db:
                if item.name == key:
                    return item
            raise KeyError(key)
If a string is given, and no record with that name is found, the method simply raises the builtin KeyError exception, allowing the caller of the method to decide how to handle this case. Now we can access the database through subscripts:
>>> import frienddb
>>> fdb = frienddb.Frienddb('friend.db')
>>> fdb[0]
Name: Harry Smith
Phone: 443-2199
Email: hsmith@notmail.com
>>> fdb[1]
Name: Sue Jones
Phone: 332-1991
Email: sue@sue.net
>>> fdb['Harry Smith']
Name: Harry Smith
Phone: 443-2199
Email: hsmith@notmail.com


next up previous contents
Next: Inheritance Up: Writing Modules Previous: Private Attributes   Contents
Phil Spector 2003-11-12