I l@ve RuBoard |
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).
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.
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.
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 |