I l@ve RuBoard Previous Section Next Section

9.5 Bigger Examples

9.5.1 Compounding Your Interest

Someday, most of us hope to put a little money away in a savings account (assuming those student loans ever go away). Banks hope you do too, so much so that they'll pay you for the privilege of holding onto your money. In a typical savings account, your bank pays you i nterest on your principal. Moreover, they keep adding the percentage they pay you back to your total, so that your balance grows a little bit each year. The upshot is that you need to project on a year-by-year basis if you want to track the growth in your savings. This program, interest.py , is an easy way to do it in Python:

trace = 1  # print each year?

def calc(principal, interest, years):
    for y in range(years):
        principal = principal * (1.00 + (interest / 100.0))
        if trace: print y+1, '=> %.2f' % principal
    return principal

This function just loops through the number of years you pass in, accumulating the principal (your initial deposit plus all the interest added so far) for each year. It assumes that you'll avoid the temptation to withdraw money. Now, suppose we have $65,000 to invest in a 5.5% interest yield account, and want to track how the principal will grow over 10 years. We import and call our compounding function passing in a starting principal, an interest rate, and the number of years we want to project:

% python
>>> from interest import calc
>>> calc(65000, 5.5, 10)
1 => 68575.00
2 => 72346.63
3 => 76325.69
4 => 80523.60
5 => 84952.40
6 => 89624.78
7 => 94554.15
8 => 99754.62
9 => 105241.13
10 => 111029.39
111029.389793

and we wind up with $111,029. If we just want to see the final balance, we can set the trace global (module-level) variable in interest to before we call the calc function:

>>> import interest
>>> interest.trace = 0
>>> calc(65000, 5.5, 10)
111029.389793

Naturally, there are many ways to calculate compound interest. For example, the variation of the interest calculator function below adds to the principal explicitly, and prints both the interest earned (earnings) and current balance (principal) as it steps through the years:

def calc(principal, interest, years):
    interest = interest / 100.0
    for y in range(years):
        earnings  = principal * interest
        principal = principal + earnings
        if trace: print y+1, '(+%d)' % earnings, '=> %.2f' % principal
    return principal

We get the same results with this version, but more information:

>>> interest.trace = 1
>>> calc(65000, 5.5, 10)
1 (+3575) => 68575.00
2 (+3771) => 72346.63
3 (+3979) => 76325.69
4 (+4197) => 80523.60
5 (+4428) => 84952.40
6 (+4672) => 89624.78
7 (+4929) => 94554.15
8 (+5200) => 99754.62
9 (+5486) => 105241.13
10 (+5788) => 111029.39
111029.389793

The last comment on this script is that it may not give you exactly the same numbers as your bank. Bank programs tend to round everything off to the cent on a regular basis. Our program rounds off the numbers to the cent when printing the results (that's what the %.2f does; see Chapter 2 for details), but keeps the full precision afforded by the computer in its intermediate computation (as shown in the last line).

9.5.2 An Automated Dial-Out Script

One upon a time, a certain book's coauthor worked at a company without an Internet feed. The system support staff did, however, install a dial-out modem on site, so anyone with a personal Internet account and a little Unix savvy could connect to a shell account and do all their Internet business at work. Dialing out meant using the Kermit file transfer utility.

One drawback with the modem setup was that people wanting to dial out had to keep trying each of 10 possible modems until one was free (dial on one; if it's busy, try another, and so on). Since modems were addressable under Unix using the filename pattern /dev/modem*, and modem locks via /var/spool/locks/LCK*modem*, a simple Python script was enough to check for free modems automatically. The following program, dokermit, uses a list of integers to keep track of which modems are locked, glob.glob to do filename expansion, and os.system to run a kermit command when a free modem has been found:

#!/usr/bin/env python
# find a free modem to dial out on

import glob, os, string
LOCKS = "/var/spool/locks/"

locked = [0] * 10
for lockname in glob.glob(LOCKS + "LCK*modem*"):    # find locked modems
    print "Found lock:", lockname
    locked[string.atoi(lockname[-1])] = 1           # 0..9 at end of name

print 'free: ',
for i in range(10):                                 # report, dial-out
    if not locked[i]: print i,
print

for i in range(10):
    if not locked[i]:
        if raw_input("Try %d? " % i) == 'y':
            os.system("kermit -m hayes -l /dev/modem%d -b 19200 -S" % i)
            if raw_input("More? ") != 'y': break

By convention, modem lock files have the modem number at the end of their names; we use this hook to build a modem device name in the Kermit command. Notice that this script keeps a list of 10 integer flags to mark which modems are free (1 means locked). The program above works only if there are 10 or fewer modems; if there are more, you'd need to use larger lists and loops, and parse the lock filename, not just look at its last character.

9.5.3 An Interactive Rolodex

While most of the preceding examples use lists as the primary data structures, dictionaries are in many ways more powerful and fun to use. Their presence as a built-in data type is part of what makes Python high level, which basically means "easy to use for complex tasks." Complementing this rich set of built-in data types is an extensive standard library. One powerful module in this library is the cmd module that provides a class Cmd you can subclass to make simple command-line interpreter. The following example is fairly large, but it's really not that complicated, and illustrates well the power of dictionaries and of reuse of standard modules.

The task at hand is to keep track of names and phone numbers and allow the user to manipulate this list using an interactive interface, with error checking and user-friendly features such as online help. The following example shows the kind of interaction our program allows:

% python rolo.py
Monty's Friends: help                       

Documented commands (type help <topic>):
========================================
EOF             add             find            list            load
save

Undocumented commands:
======================
help

We can get help on specific commands:

Monty's Friends: help find              # compare with the help_find() method
Find an entry (specify a name)

We can manipulate the entries of the Rolodex easily enough:

Monty's Friends: add larry                  # we can add entries
Enter Phone Number for larry: 555-1216
Monty's Friends: add                        # if the name is not specified...
Enter Name: tom                             # ...the program will ask for it
Enter Phone Number for tom: 555-1000
Monty's Friends: list
=========================================
               larry : 555-1216
                 tom : 555-1000
=========================================
Monty's Friends: find larry
The number for larry is 555-1216.
Monty's Friends: save myNames             # save our work
Monty's Friends: ^D                       # quit the program  (^Z on Windows)

And the nice thing is, when we restart this program, we can recover the saved data:

% python rolo.py                       # restart
Monty's Friends: list                  # by default, there is no one listed
Monty's Friends: load myNames          # it only takes this to reload the dir
Monty's Friends: list
=========================================
               larry : 555-1216
                 tom : 555-1000
=========================================

Most of the interactive interpreter functionality is provided by the Cmd class in the cmd module, which just needs customization to work. Specifically, you need to set the prompt attribute and add some methods that start with do_ and help_. The do_ methods must take a single argument, and the part after the do_ is the name of the command. Once you call the cmdloop() method, the Cmd class does the rest. Read the following code, rolo.py, one method at a time and compare the methods with the previous output:

#!/usr/bin/env python 
# An interactive rolodex

import string, sys, pickle, cmd

class Rolodex(cmd.Cmd):

    def __init__(self):
        cmd.Cmd.__init__(self)              # initialize the base class
        self.prompt = "Monty's Friends: "   # customize the prompt
        self.people = {}                    # at first, we know nobody

    def help_add(self): 
        print "Adds an entry (specify a name)"
    def do_add(self, name):
        if name == "": name = raw_input("Enter Name: ")
        phone = raw_input("Enter Phone Number for "+ name+": ")
        self.people[name] = phone           # add phone number for name

    def help_find(self):
        print "Find an entry (specify a name)"
    def do_find(self, name):
        if name == "": name = raw_input("Enter Name: ")
        if self.people.has_key(name):
            print "The number for %s is %s." % (name, self.people[name])
        else:
            print "We have no record for %s." % (name,)

    def help_list(self):
        print "Prints the contents of the directory"
    def do_list(self, line):        
        names = self.people.keys()         # the keys are the names
        if names == []: return             # if there are no names, exit
        names.sort()                       # we want them in alphabetic order
        print '='*41
        for name in names:
           print string.rjust(name, 20), ":", string.ljust(self.people[name], 20)
        print '='*41

    def help_EOF(self):
        print "Quits the program"
    def do_EOF(self, line):
        sys.exit()

    def help_save(self):
        print "save the current state of affairs"
    def do_save(self, filename):
        if filename == "": filename = raw_input("Enter filename: ")
        saveFile = open(filename, 'w')
        pickle.dump(self.people, saveFile)

    def help_load(self):
        print "load a directory"
    def do_load(self, filename):
        if filename == "": filename = raw_input("Enter filename: ")
        saveFile = open(filename, 'r')
        self.people = pickle.load(saveFile) # note that this will override
                                            # any existing people directory

if __name__ == '__main__':               # this way the module can be
    rolo = Rolodex()                     # imported by other programs as well
    rolo.cmdloop()

So, the people instance variable is a simple mapping between names and phone numbers that the add and find commands use. Commands are the methods which start with do_ , and their help is given by the corresponding help_ methods. Finally, the load and save commands use the pickle module, which is explained in more detail in Chapter 10.

How Does the Cmd Class Work, Anyway?

To understand how the Cmd class works, read the cmd module in the standard Python library you've already installed on your computer.

The Cmd interpreter does most of the work we're interested in its onecmd() method, which is called whenever a line is entered by the user. This method figures out the first word of the line that corresponds to a command (e.g., help, find, save, load, etc.). It then looks to see if the instance of the Cmd subclass has an attribute with the right name (if the command was "find tom", it looks for an attribute called do_find). If it finds this attribute, it calls it with the arguments to the command (in this case 'tom'), and returns the result. Similar magic is done by the do_help() method, which is invoked by this same mechanism, which is why it's called do_help()! The code for the onecmd() method once looked like this (the version you have may have had features added):

# onecmd method of Cmd class, see Lib/cmd.py
def onecmd(self, line):         # line is something like "find tom"
    line = string.strip(line)   # get rid of extra whitespace
    if not line:                # if there is nothing left, 
        line = self.lastcmd     # redo the last command
    else:
        self.lastcmd = line     # save for next time
    i, n = 0, len(line)
                                # next line finds end of first word
    while i < n and line[i] in self.identchars: i = i+1
                                # split line into command + arguments
    cmd, arg = line[:i], string.strip(line[i:])
    if cmd == '':               # happens if line doesn't start with A-z
        return self.default(line)
    else:                       # cmd is 'find', line is 'tom'
        try:
            func = getattr(self, 'do_' + cmd)  # look for method
        except AttributeError:
            return self.default(line)
        return func(arg)         # call method with the rest of the line

This example demonstrates the power of Python that comes from extending existing modules. The cmd module takes care of the prompt, help facility, and parsing of the input. The pickle module does all the loading and saving that can be so difficult in lesser languages. All we had to write were the parts specific to the task at hand. The generic aspect, namely an interactive interpreter, came free.

I l@ve RuBoard Previous Section Next Section