In the previous chapter, we learned how to invent the ``little parts'' of programming by writing functions. This chapter, we learn about the ``big parts'' --- modules.
Another reason for subassemblies is design: it is easy to update and improve a product by merely disconnecting a subassembly and replacing it by another. Auto companies do this all the time when they issue a new ``model'' of car. Yet another reason is economy: if the subassemblies are built so that they easiler connect to other subassemblies, then the same subassembly can be used in distinct products. Again, the automakers are experts at using the same engine assembly in many different versions of car.
Subassemblies are used outside of manufacturing, too. For example, a book is organized into chapters, and a video game has levels. But there are even more interesting forms: Any series of books or video games (Mario Brothers and Star Trek, to mention two examples) reuse the same characters with the same personalities in each book or game. The characters are ``literary subassemblies'' that are ``connected'' to the books and games. Working with a fixed cast of characters is efficient, promotes a uniform design, and lets us reuse actors who people enjoy.
A book series's collection of characters, locales, tools, and terminology (think again of Star Trek or Mario Brothers) form a specific world in which new books and games are easily invented and enjoyed. Such a world is called a domain, and the people, things, and concepts in the domain form a domain-specific language. Domain-specific languages are a powerful tool for production of quality cars, televisions, books, and games. And, the idea also applies to program construction!
Modules are used for efficiency reasons: distinct teams build distinct modules, speeding the development and letting each team do a good job on their specific part of the program.
Modules are also used for design reasons: When a module of the program must be repaired or improved, the entire module is removed and replaced with a newly written one.
Modules are also used for economy: Modules are often written so that they can be used within many different programs. For example, a module that codes the layout of a chess board and the behaviors of chess pieces can be used in various chess-playing programs.
Finally, when a program solves a problem in a standard problem area (domain), it is useful to collect together into a module the functions --- the ``little pieces'' --- that helped solve the problem. Such a module is the starting point of a domain-specific programming language for program building in the problem domain.
A module is constructed so that it has
Another form of module lacks start-up commands, that is, it does computation only when its functions are called/contacted by other modules. We saw examples of this in the previous chapter: a bank account and its maintenance functions, a telephone book and its maintenance functions, and a slide puzzle and its maintenance functions. Each of these examples models a physical object, and for this reason, such a module is called a model module. We saw how an ATM program module might call the functions of the bank-account module module to permit cash withdrawals. In this chapter, we learn how to place the program module in one file and the bank-account model in another file, and link them together.
In Chapter 4, we saw how to read a line of text and divide it into words. Then we saw how to rearrange the words into ``Yoda sentences.'' We developed the resulting program in two stages: The first program, Words.py, read a sentence as input and produced as its output a tuple that contains all the words in the input sentence. The second program, Rearrange.py, rearranged the tuple's words into a ``Yoda sentence.''
It is sensible to connect the two programs together in a sequence,
like this:
Each program is in fact a program module, and we can connect together the two program modules so that the second one uses the results from the first. This approach is the oldest known use of modules, and we use it as a motivating example.
Let's begin with two tiny
program modules and see how to connect them in sequence. Here is a program
module, Square:
===========================
# A module begins with a documentation string:
"""Square computes the square of an int
assumed input: an int
guaranteed output: the int's value squared, saved in variable sq
"""
input = int(raw_input("Please type an int: "))
sq = input * input
====================================
The program module has no functions that can be called by other
modules, and its only ``data structures'' are its variables,
input and sq.
Now, start the Python interpreter, and in response to the prompt,
>>>, type the following:
>>> import Square
You have just demanded that module Square be imported
into the computer, and
you will see this in the interpreter's window:
Please type an int:
This shows that Square has been copied (loaded)
into primary storage
and started.
Say that you type 3 and press Enter. You will see merely
>>>
Although the
program module has finished all its instructions its
namespace
(that is, its variables and their values) is still living in computer storage,
and you can access it.
Type this:
>>> Square.sq
and you will see printed, 9 --- you have just
demanded the value of variable sq from within the namespace of
module Square.
If you like, you can compute with the value:
>>> j = Square.sq
>>> j * j
This prints 81.
In this simple way, you have ``contacted'' the ``subassembly'' (module)
for information that you used to compute a multiplication.
We can write a second program module that does what we did with our
fingers and the keyboard; here it is:
==============================================
"""Fourth uses module Square to compute the fourth power of an int."""
import Square
j = Square.sq # contact Square to learn the value of sq
print j * j
raw_input("\npress Enter to finish")
=================================================
Here is what you see when you start this program:
$python Fourth.py
Please type an int: 3
81
This shows that Fourth was started; it imported
Square; the latter its work; and
Fourth extracted the value of sq from Square's
namespace
and finished the computation.
Notice if you write the following in program Fourth:
import Square
j = Square.sq
import Square
this does not execute Square twice! (Try it and see.)
A module is not an ``oversized function'' --- it is a subassembly
that is loaded and started just once.
(Later, we will learn
how to place functions in a module, import the module once,
and then call the module's functions over and over.)
Let's study what happens inside the computer when this example executes.
When we start, python Fourth.py, we have this configuration:
Program module Fourth.py has been loaded into computer storage,
and it has just started execution.
The instruction counter (i.c.) marks the first command in
Fourth to execute, and the namespace pointer,
n.s., remembers that Fourth's namespace is in use.
The first command, import Square, pauses execution within
Fourth, loads module Square into storage,
and constructs a new namespace for the new module:
The computer's instruction counter (i.c.) resets
to the commands within Square, and the
namespace variable (n.s.) resets to Square's namespace.
Square's (start-up) commands execute --- this reads the user's input
and calculates its square:
The computer constructs
variables input and sq within
Square's namespace because n.s. correctly remembers
that commands in Square are executing.
Once program module Square finishes its
start-up commands, the instruction counter and
namespace pointer reset to Fourth, but Square's
namespace remains. In addition, a variable named Square
is constructed within Fourth's namespace --- since Fourth
imported Square, it is linked to it, meaning that
it can contact the module and look into its namespace:
So, the command, j = Square.sq, computes as follows:
If you wish, you can think of a module's namespace as a dictionary data structure. Indeed, it is only for historical reasons that the lookup of sq in Square is written Square.sq (and not as Square[sq]).
When Fourth finishes its start-up commands,
the configuration looks like this:
Both namespaces rest in storage, waiting for contact from other modules.
$ python Yoda.py Type a sentence: take the right turn Yoda's sentence: right the take turn!We assemble the Yoda program from its two program modules; the first reads a sentence a user types and constructs a tuple of the sentence's words, and the second module uses the tuple of words to build a Yoda sentence.
Here is the Words module, repeated verbatim from
Chapter 4:
FIGURE==========================================================
"""Words extracts the words from a line of text.
assumed input: a sentence typed as a single line of text
guaranteed output: the words, listed in the order that
they appeared in the sentence, saved in variable, tuple.
"""
separators = (".", ",", ";", ":", "-", "?", "!", " ")
sentence = raw_input("Type a sentence: ")
tuple = () # holds the tuple of words we collect
next_word = "" # holds the characters of each word that we collect
for char in sentence :
# invariant: all chars examined so far are grouped into words in tuple
# along with the word we are collecting into next_word
if char in separators : # have we reached the end of a word ?
if next_word != "" : # have we collected a nonempty word ?
tuple = tuple + (next_word,) # add next_word to the tuple
next_word = "" # prepare to collect another word
else :
next_word = next_word + char # add char to the word we are building
if next_word != "" : # oops --- did sentence end without closing punctuation?
tuple = tuple + (next_word,)
# print tuple # printing the output from the first step is optional
ENDFIGURE=========================================================
Next,
here is a minor rewrite of module, Rearrange,
which imports module Words and then contacts it:
FIGURE==========================================================
""" Rearrange builds a ``Yoda'' sentence, that is, a sentence whose word
order is changed.
assumed input: the output from module Words --- a tuple of words
guaranteed output: the words rearranged into a Yoda sentence
"""
import Words # load and link to the module, Words.py
tuple = Words.tuple # contact module Words to learn the value of its tuple
import random # load and link to the random-number-generator module
yoda_sentence = "" # the sentence we will build
while len(tuple) != 0 :
# invariant: the words extracted from tuple are in yoda_sentence
# print tuple, yoda_sentence # print trace info
choice = random.randrange(len(tuple)) # choose a word to extract
yoda_sentence = yoda_sentence + " " + tuple[choice]
tuple = tuple[:choice] + tuple[choice+1:] # rebuild tuple without the choice
print "\nYoda's sentence:"
print yoda_sentence + "!"
raw_input("\npress Enter to finish")
ENDFIGURE=========================================================
The linking and contacting are done by the first two commands:
Words is imported, which makes it compute a tuple of
words.
Next, the value of tuple
from Word's namespace is extracted, via
tuple = Words.tuple
and saved in a new variable, also named tuple,
in Rearrange's namespace.
Then random is imported. (Module random is already written and comes with the Python interpreter.) Module random contains functions that can be contacted to return random numbers. One of the these functions, randrange, is called by Fourth.
Here is a picture of the software architecture
of the program we just assembled:
The solid arrows show that module Rearrange imports both
Words and
random; the dashed arrows show the direction in which
information travels from one module to another.
Such diagrammatic
``blueprints'' are commonly used by software architects to design and
document large programs.
Because the program's information travels primarily in a sequence, from the input keyboard to one module to the next to the output display, the software architecture is called sequential.
The previous chapter emphasized that functions are useful for maintaining a data structure and the functions that do the maintenance should be placed with the data structure. This is exactly the principle behind a model module.
A model module is a data structure and a collection of functions that maintain the data structure. A model has no significant start-up commands, and it is not intended to compute all alone. Instead, it assists other modules with their computations: the other modules call the functions of the model module for assistannce.
Model modules are often used to ``model'' a physical object, like a gameboard or a telephone book or a collection of bank accounts (``cashboxes''). A data structure represents the physical object, and maintenance functions code the actions that one does with the object.
A model module is often considered the ``brain'' of a computer program, because it remembers the information needed to solve a problem. We now review three examples of model modules encountered in the previous chapter.
Here is the telephone book example from the previous chapter;
when it is saved in a file by itself, it is a model module:
FIGURE============================================
"""TelephoneBook : It maps a person's name to their phone number,
e.g., tel_book={"Jane Sprat": 4098, "Jake Jones": 4139, "Lana Lang": 2135},
and it contains maintenance functions for insert, finding, and deleting
entries.
"""
tel_book = { } # the data structure
# The functions that maintain the telephone book:
def lookup(name) :
"""lookup finds the phone number for name and returns it
parameter: name - the person we look for
returns: the phone number for name. If name is not in the book,
-1 is returned
"""
global tel_book
number = -1
if name in tel_book :
number = tel_book[name]
return number
def insert(name, number) :
"""insert inserts the new name and number into the book.
If the name is already in the phone book, _no action is taken_!
parameters: name - the person; number - their phone number
"""
global tel_book
if not(name in tel_book) :
tel_book[name] = number
def delete(name) :
"""delete removes the entry for name in the book."""
global tel_book
if name in tel_book :
del tel_book[name]
ENDFIGURE=================================================
The module ``models'' a telephone book inside the computer.
It has a data structure and functions that can be contacted by
other modules but no start-up commands that compute on their own.
A model module is meant to assist other modules.
Here is how another module might use the model module:
import TelephoneBook
. . .
someone = raw_input("Type name of person: ")
number = TelephoneBook.lookup(someone)
print someone, "has phone number ", number
. . .
The a function saved within a module is referenced
by stating the module name, then a dot, then the function name
(e.g., TelephoneBook.lookup(...). This contacts the module
and calls its lookup function, which
operates with its own namespace and the namespace of the module
in which it is embedded. The function is an entry point
to the data structure held within the module.
We can use the telephone-book model module in an application that
lets a person insert names into a telephone book and do
lookups from the book. The module we will write ``controls'' the actions
that a human takes upon
the telephone book, and for this reason it is
sometimes called a controller module.
Here is the algorithm for the controller module:
while True
read a command from the user
if the command is ``insert'',
then insertNewPerson into the telephone book
else if the command is ``lookup'',
then lookupPerson in the telephone book
else error
We can convert the algorithm into Python code, where the phrases,
insertNewPerson and lookupPerson become
calls to functions that use the telephone-book module. Here is the program
module based on the algorithm, where the two helper functions are listed
first:
FIGURE====================================================
# UseTelBook helps a human use an electronic phone book.
import TelephoneBook
# Helper functions for the controller algorithm:
def insertNewPerson() :
"""insertNewPerson requests a name and number from
the user and inserts them into the phone book.
"""
name = raw_input("Please type a new name: ")
num = int(raw_input("Please type their phone number: "))
TelephoneBook.insert(name,num)
def lookupPerson() :
"""lookupPerson requests a name and finds that
name in the phone book.
"""
name = raw_input("Please type a name to lookup: ")
print "The phone number for", name, "is", TelephoneBook.lookup(name)
### The controller algorithm starts here:
while True :
request = raw_input("Type i(nsert) or l(ookup): ")
if request[0] == "i" :
insertNewPerson()
elif request[0] == "l" :
lookupPerson()
else :
print "Invalid request"
ENDFIGURE======================================================
The software architecture
looks like this:
This form of architecture, where the model module is the ``brain''
of the program and another module controls the actions
that a human makes on the model, is called a
model-view-controller (MVC) architecture.
The ``view assembly'' in the architecture is the computer keyboard (``input view'') and the display screen (``output view''), so called because they give the human a ``view'' of what the model is and what it knows.
The controller module acts like the controls on the panel of a radio or televison --- it provides the human a means of adjusting the model. The adjustments are transmitted via the input view and results are displayed in the output view.
Since the application is divided into modules, it is straightforward
to ``unplug'' one module and replace it by an alternate version.
For example, perhaps we decide the change the model module so that
the telephone book is modeled by a list of name,number pairs
rather than by a dictionary --- We merely replace the existing
module, TelephoneBook.py, by this one:
=======================================
# TelephoneBook holds the data structure and maintenance functions
# for a telephone book.
# The data structure is a list of (name,number) pairs
# Example: book = [("Mary Smith", 3453), ("Jake Jones", 2458)]
book = [] # the book starts empty
# Here are two helper functions:
def printBook() :
"""printBook prints the entire contents of the phone book"""
global book
for entry in book :
print entry[0] + ":", entry[1]
def inBook(name) :
"""inBook checks if name is already in the phone book.
parameter: name, a string - a person's name
returns: the index number of where the name is found in the book;
returns -1, if the name is not in the book
"""
global book
answer = -1
index = 0
for entry in book :
if entry[0] == name :
answer = index
break
else :
index = index + 1
return answer
# Here are the functions used by the other modules:
def insert(name, number) :
"""addPerson adds a new name and number to the phone book,
provided that the name is not already in the book.
parameters: name - a string, the person's name
number - an int, the four-digit phone number
"""
global book
if inBook(name) == -1 : # name not in the book?
book = book + [(name,number)]
else :
print "error: " + name + " is already in the book"
def lookup(name) :
"""lookup searches the phone book for name.
parameter: name - a string, the person's name
returns: the phone number, an int, for the person
(if the name isn't in the book, -1 is returned)
"""
global book
number = -1
for entry in book :
if entry[0] == name :
number = entry[1]
break
return number
def delete(name) :
"""delete removes the entry for name in the book.
parameter: name - the name to be removed
"""
global book
where = inBook(name)
if where != -1 :
book = book[:where] + book[where+1:]
========================================
Because the module's functions have the same name, the same
parameter structure, and the same behavior,
the new module can replace the old one.
Note that the new module has some additional functions,
but this causes no trouble for the program that imports and uses
the module.
======================================== # BankDatabase is a simple simulation of a bank's database. # The database remembers the cash saved for each account number. # This is done with a list of ints. For example, the list, # all_accounts = [10050, 3000, 25066] # has recorded that # acouunt 0 has a balance of $100.50, and # account 1 has a balance of $30.00, and # account 2 has a balance of $250.66 # Here are some sample accounts to get started. # Usually, this information is read from a disk file. all_accounts = [10050, 3000, 25066] # Maintenance functions for the database. # The bank's main office uses these functions to create new accounts and # deposit money into them: def addAccount(cash) : """addAccount creates a new bank account and assigns to it an account number parameter: cash - an int, the initial deposit returns: the account number, an int """ global all_accounts all_accounts = all_accounts + [cash] new_account_num = len(all_accounts) - 1 return new_account_num def deposit(account_num, cash) : """deposit adds cash to an account. parameters: account_num - an int, the account number cash - an int, the amount to deposit """ global all_accounts all_accounts[account_num] = all_accounts[account_num] + cash # The ATMs send input to these functions, which compute and return answers: def getBalance(account_num) : """getBalance returns the account's balance parameter: account_num - an int, the account number returns the balance, in cents """ global all_accounts return all_accounts[account_num] def withdraw(account_num, cash) : """withdraw removes cash from balance. If cash is greater than balance, then only the remaining balance is withdrawn. parameters: account - an int, the account number cash, an int, a cents-only amount returns the amount of cash withdrawn from the balance """ global all_accounts balance = all_accounts[account_num] if cash > balance : amount_withdrawn = balance else : amount_withdrawn = cash # revise account info: all_accounts[account_num] = balance - amount_withdrawn return amount_withdrawn =========================================================And here is the ATM controller that lets the human use the database:
FIGURE=================================================== # ATM lets a human withdraw cash from an automated teller machine. # Inputs: the account number on the person's bank card # the dollars and cents amount to withdraw # Outputs: the amount of money withdrawn (and also, the cash!) # and the new account balance import BankDatabase # connect this program to the bank's program ##### Some helper functions that reduce copy-and-paste: def errorMessage(message) : """errorMessage prints a specific error message. parameter: message - a string, stating the error """ text = "Error: " + message print text + " Sorry.\n" def formatDollarsCents(amount) : """formatDollarsCents formats a cents amount into a dollars.cents string parameter: amount - a nonegative int, a cents-only amount precondition: amount >= 0 returns: a string, formatted as dollars.cents. (If amount is < 0, then the string, 'negative' is returned.) postcondition: answer == "$D.C" and (D * 100) + C == amount """ if amount >= 0 : dollars = amount / 100 cents = amount % 100 import string # load string module with helper functions answer = "$" + str(dollars) + "." + string.zfill(cents, 2) else : answer = "negative" return answer ##### The program starts here: print "Please insert your ATM card so that" account = int(raw_input("I can read your account number: ")) # NOTE: a real ATM would use a different operation than raw_input OK = True dollars = int(raw_input("Type dollars amount to withdraw: ")) if dollars < 0 : errorMessage("You typed a negative number.") OK = False if OK : cents = int(raw_input("Type cents amount to withdraw: ")) if (cents < 0) or (cents > 99) : errorMessage("The cents amount was not in the range 0..99.") OK = False if OK : request = (dollars * 100) + cents print "\nRequest to withdraw " + formatDollarsCents(request) money = BankDatabase.withdraw(account, request) # NOTE: if the program was wired to a real ATM, the ATM would now # dispense the requested money print formatDollarsCents(money), "withdrawn" new_balance = BankDatabase.getBalance(account) print "Balance is", formatDollarsCents(new_balance) raw_input("\nTransaction completed") ENDFIGURE================================================There are several advantages to organizing the bank's database and its ATMs into modules:
The previous chapter introduced a simple board game --- the slide puzzle.
As an exercise, we can define a slide-puzzle module, which contains
the data-structure representation of the puzzle and its key maintenance
functions:
========================================================
# SlidePuzzle models a slide puzzle holding numbers and one empty space.
PUZZLE_SIZE = 4 # the puzzle's size
puzzle = [ [15, 14, 13, 12], # the puzzle. 0 marks the empty space
[11, 10, 9, 8],
[ 7, 6, 5, 4],
[ 3, 2, 1, 0] ]
# data invariant: puzzle's numbers form a permutation of 0..15
empty_space = (3, 3) # remembers the location of the empty space (0)
def move(num) :
"""move attempts to move the piece labelled by num into the empty space.
parameter: num - the number to move; must be adjacent to the empty space.
precondition: num > 0 and num < (PUZZLE_SIZE * PUZZLE_SIZE)
returns: True, if num moves into the empty space; returns False otherwise.
"""
global PUZZLE_SIZE, puzzle, empty_space # we use all three variables....
success = False
piece = () # will remember the cell coordinates where num rests
for i in range(PUZZLE_SIZE): # search for num in puzzle:
for j in range(PUZZLE_SIZE) :
if num == puzzle[i][j] :
piece = (i, j) # we found num at coordinates i,j
if piece != () : # we found num; let's try to move it:
i = piece[0]
j = piece[1]
if empty_space == (i-1, j) \ # adjacent to the empty space?
or empty_space == (i+1, j) \
or empty_space == (i, j-1) \
or empty_space == (i, j+1) :
# if True, it's ok to move num into the empty space:
puzzle[empty_space[0]][empty_space[1]] = num
puzzle[i][j] = 0 # the new empty space
empty_space = (i, j)
success = True
return success
def toString() :
"""toString builds a string that shows the puzzle's contents.
returns: the string
"""
global puzzle
import string
answer = ""
for row in puzzle :
for piece in row :
if piece == 0 : # empty space ?
text = "__"
else :
text = string.zfill(square, 2)
answer = answer + text + " "
answer = answer + "\n"
return answer
=============================================================
This module is typical for gameboards: It defines the game
board and initializes it with its squares and playing pieces.
It contains a maintenance function, move,
that does the mechanics of moving
a playing piece on the board. It also contains a function,
toString that
returns a string representation of the board for printing.
Here is how we use SlidePuzzle.py to build a simple
controller module for the game board:
===========================
import Puzzle
while True :
print Puzzle.toString()
num = int(raw_input("Type number of piece to move: "))
OK = Puzzle.move(num) # try to move the piece showing num
if not OK :
print "Illegal move --- try again"
================================
One big advantage of separating the slide-puzzle module from the rest of the game is that we can revise the controller module and especially the puzzle's view (say, by writing a GUI --- graphical user interface) without altering the puzzle itself. The puzzle is the ``brain'' of the game.
A data invariant for the data structure in a module is sometimes called the module invariant.
Please review the BankAccount module and the SlidePuzzle modules in the previous section. Both have data structures that are documented with important data invariants. Next, review the discussion in the previous chapter, which explained why the BankAccount's withdraw function maintained the data invariant for the balance. And, review the discussion there that justifies why the move function maintains the puzzle's data invariant.
When we collect a data structure and its maintenance functions within one module, we can enforce the data (module) invariant entirely within the one module. (This assumes all other modules contact this one via its functions only.) For this strong reason, we try to employ model modules with module invariants as often as possible.
When we play a card game, we do so with a deck of cards. Most card games use a ``dealer,'' who is the person who gives the cards, one by one to the players. (When you play solitaire, you are both the ``player'' and the ''dealer.'')
Let's design a basic card game, where we model the deck of cards inside the computer, and we write a dealer that gives the human user cards one at a time. (We won't bother here to make the dealer enforce the rules of any particular game; the dealer will give cards to the human until the human says, ``stop.'')
$ python Cards\Main.py Your hand is: [] Would you like another card (y or n)? y Your hand is: [(5, 'diamonds')] Would you like another card (y or n)? y Your hand is: [(5, 'diamonds'), (7, 'spades')] Would you like another card (y or n)? nThat is, the program repeatedly shows the user her hand of cards and asks if another card is desired. To model this behavior, we will build a card deck and an algorithm that extracts cards from the deck and ``gives'' them to the user. The algorithm that extracts cards is the ``dealer.''
The CardDeck module models a deck of cards; the Dealer holds the algorithm that interacts with the user and the CardDeck. The Dealer's algorithm is simple and will look like this:
while not yet finished : show the user her current hand of cards ask the user if she wants one more card if yes : get one more card from the CardDeck add the card to the user's hand else (no) : we are finished, so quit the loop
A deck is a collection of cards, which we model, say, as a list of computerized
cards:
deck = [ (ace, spades), (2, spades), ..., (king, spades), (ace, hearts), ... ]
We should write an initialize function that generates the
cards and places them in deck.
When we study the algorithm for the Dealer, we see that the only operation that the Dealer makes on the CardDeck is dealing a card. Let's call this operation getCard.
Here is what we have designed so far:
===============
"""CardDeck models a deck of playing cards, where each card is a pair
of the form, (spots, suit). Examples: ("ace","spades") or (9,"hearts).
Operations: initialize (constructs a new deck of cards),
getCard (extracts and returns a card from the deck)
"""
deck = [] # the deck of cards, where each card is a pair, (count, suit)
def initialize() :
"""initialize generates 52 cards and adds them to the deck"""
pass # we must code this
def getCard() :
"""getCard deals a card from the deck
returns: the card dealt - it is a pair of the form, (count, suit).
"""
pass # we must code this
=========================
The initialize function must insert 13 spades, 13 hearts, 13
diamonds, and 13 clubs into the deck. It is best to write a helper
function, add, that inserts one suit of cards and call the
function four times, like this:
def initialize() :
add("spades")
add("hearts")
add("diamonds")
add("clubs")
The add function will merely count,
"ace" 2, 3, 4, ..., 10, "jack", "queen", "king" add to the
deck one card
of each count for each suit.
We'll study its coding a bit later.
The cards must also be shuffled; this is done with the help of a
function from the random module:
import random
random.shuffle(deck) # mix the cards
The getCard function merely removes a card from the shuffled deck.
Here is what we have coded for the CardDeck:
FIGURE==================================
"""CardDeck models a deck of playing cards, where each card is a pair
of the form, (count, suit). Examples: ("ace","spaces") and (9,"hearts").
Operations: initialize (constructs a new deck of cards),
getCard (extracts and returns a card from the deck)
"""
deck = [] # the deck of cards, where each card is a pair, (count, suit)
# data invariant: deck holds a subset of 52 unique cards, shuffled
def initialize() :
"""initialize generates 52 unique cards and assigns them to the deck"""
global deck # so, we are obligated to maintain deck's invariant!
add("spades")
add("hearts")
add("diamonds")
add("clubs")
import random
random.shuffle(deck) # mix the cards
# at this point, deck's invariant is maintained
def getCard() :
"""getCard deals a card from the deck.
precondition: deck is nonempty
returns: the card dealt - it is a pair of the form, (count, suit).
postcondition: deck_new.append(card) == deck_old
"""
global deck # we must maintain deck's invariant
if (len(deck) == 0) :
print "CardDeck error: no more cards to get"
card = ()
else :
card = deck.pop() # removes card from the end of deck and
# makes the deck smaller by one
# at this point, deck's data invariant is maintained
return card
def add(suit) :
"""add is a helper method that inserts one suit of cards into the card deck
parameter: suit - a string, the name of the suit ("clubs" or "diamonds"
or "spades" or "hearts")
"""
global deck # this function does _not_ maintain the data invariant,
# but it is called _only_ within initialize, which does!
deck.append(("ace", suit))
for i in range(2, 11) : # this counts from 2 through 10
deck.append((i,suit))
deck.append(("jack", suit))
deck.append(("queen", suit))
deck.append(("king", suit))
ENDFIGURE====================================
The functions use two new Python tricks:
We might test the module interactively the same way we tested functions:
$python -i CardDeck.py
>>> initialize()
>>> for i in range(55) :
... next_card = getCard()
... print next_card
or, we might interactively import and test the module, like a controller
module would do:
$python
>>> import CardDeck
>>> CardDeck.initialize()
>>> for i in range(55) :
... next_card = CardDeck.getCard()
... print next_card
The previous tests empty the card deck and show whether
getCard is indeed returning all the cards in the deck in a
random ordering.
def isEmpty() : """isEmpty checks if there are cards left in the deck returns True, if the deck has more cards; returns False, if the deck is empty """ . . .Test the function like this:
for i in range(110) : if CardDeck.isEmpty() : print "deck empty --- time to refill" CardDeck.initialize() # refill the deck else : print CardDeck.getCard()
while not yet finished : show the user her current deck of cards ask the user if she wants one more card if yes : get one more card from the CardDeck add the card to the user's hand else (no) : we are finished, so we quit the loopWhen we refine this algorithm, we see that we must invent a data structure that holds the cards that are already given to the user (so that we can print the cards at each iteration of the loop). We can use a list to model the player's hand of cards. Here is a quick Python coding of the algorithm:
FIGURE============================================= """Dealer controls a CardDeck so that a human can take cards one at a time.""" import CardDeck # the data structure module CardDeck.initialize() hand = [] # holds the cards that the human has requested print "I will deal you a hand of cards!\n" more_cards_needed = True while more_cards_needed : print "Your hand is:" print hand request = raw_input("\nWould you like another card (y or n)? ") if request == "n" : more_cards_needed = False elif request == "y" : card = CardDeck.getCard() hand.append(card) else : print "Sorry--bad request; try again" raw_input("\npress Enter to finish") ENDFIGURE================================================
Although Dealer.py is small, we note that the list, hand, of the user's cards is in fact another data structure that should be extracted and placed in its own module. For practice, let's do so.
Here is the revised Dealer module and another ``model'' module,
called HandOfCards:
=======================
"""Dealer2 controls a CardDeck so that a human can take cards one at a time."""
import CardDeck # the deck of cards
import HandOfCards # the user's hand of cards
CardDeck.initialize()
print "I will deal you a hand of cards!\n"
more_cards_needed = True
while more_cards_needed :
print "Your hand is:"
HandOfCards.showCards()
request = raw_input("\nWould you like another card (y or n)? ")
if request == "n" :
more_cards_needed = False
elif request == "y" :
HandOfCards.addCard(CardDeck.getCard())
else :
print "Sorry--bad request; try again"
raw_input("\npress Enter to finish")
==========================================
=======================================
"""HandOfCards holds the cards requested by the user.
Operations: showCards - prints the contents of the hand
addCard - adds one more card to the user's hand
"""
hand = [] # holds the cards that the human has requested
# invariant: holds a subset of the 52 possible cards
def toString() :
"""toString returns a representation of the hand"""
global hand
answer = ""
for card in hand :
answer = answer + (card[0] + " of " + card[1]) + "\n"
return answer
def addCard(card) :
"""addCard adds one more card to the user's hand.
parameter card - the card to be added
"""
global hand
hand.append(card)
================================================
The HandOfCards module is tiny, but it was good to
extract because it streamlined the controller module,
Dealer2. Here is a diagram of what we have built:
Remember that the solid lines represent dependency (here, Dealer2
depends on CardDeck and HandOfCards because it calls
their functions) and the dotted lines represent data flow (a user
request causes a card to travel from CardDeck to
HandOfCards and then to the display due to printing).
controller
For multi-module programs, comments serve two purposes:
import M ... print M.__doc__if module M contains a function, f, we can of course print its documentation, too:
print M.f.__doc__
Improve the card-game program so that the Dealer2 module enforces the rules of some specific card game, e.g., ``21'' (``blackjack''), where the player tries to collect a hand of cards whose total counts come as close as possible to 21 without exceeding it. (Recall that face cards have a count of 10. For simplicity, assume an ace has count of 1.)
Next, write a ComputerCardPlayer module to which the Dealer2 also deals cards so that the ComputerCardPlayer competes against the program's user to obtain a higher score at playing 21. (Make the computer player ``hold'' (stop asking for cards) when the computer player has a total count of 17 or more.
dir() # lists the bindings in the current namespace dir(M) # lists the bindings in the namespace of module, M reload(M) # reimports module M, erasing its namespace and starting over
import CheckBookso that the functions (and variables) in the module can be referenced by mentioning the module's name, e.g., CheckBook.doDeposit(100, "April 1").
There is another form of importation, which allows you to reference
the variables and functions in a module directly, without stating
the module's name first. The format is
from MODULE import NAME1, NAME2, ..., NAMEi
For example,
from BankAccout import withdraw
This imports module CheckBook but makes only withdrawvisible to the program.
You can now say
cash = withdraw(1000)
and this executes withdraw in module BankAccount.
But the name, BankAccount, is not visible, so if you
try
print BankAccount.getBalance()
or even BankAccount.withdraw(1000),
you will see this message:
NameError: name 'BankAccount' is not defined
A variation of the above import statement is
from MODULE import *
Such as from BankAccout import *.
This makes all the definitions in BankAccount visible
to the program, so that you can say
from BankAccountimport *
...
cash = withdraw(1000)
print cash, getBalance()
But again, the name BankAccount
is not visible.
Finally, we can use this pattern of commands to import functions for
interactive testing:
import BankAccount
... BankAccount.withdraw(...) ...
This lets us test the function, withdraw.
After we test the function and repair it, we can retest it without
restarting the Python interpreter by stating,
reload(BankAccount)
... BankAccount.withdraw(...) ...
This reloaded the module with its repaired coding of withdraw.
The first place the Python interpreter looks when importing a module is in the same folder where execution was started. But if the module is not found there, the interpreter follows a preset search path to other folders.
To see the search path, start the Python interpreter and type the following:
$python
>>> import sys
>>> sys.path
You will see a list of folder names, something like this:
['', 'C:\\IBMTOOLS\\utils\\logger', 'C:\\Documents and Settings\\YourName',
'C:\\Python22\\DLLs', 'C:\\Python22\\lib', 'C:\\Python22\\lib\\lib-tk',
'C:\\Python22', 'C:\\Python22\\lib\\site-packages']
>>>
The folders are searched in this order for modules to import.
Some of these folders hold modules for doing graphics and animations;
in others, you can place Python modules that you would like to reuse.
Also, once you have started Python, you
can append a new path to the end of sys.path like this:
sys.path.append("C:\\Documents and Settings\\YourName\\MyPythonModules")
Or, you can define a PYTHONPATH environment variable, like you
can reset Window's PATH variable. (See the Python documentation for
information about how to do this.)
Buildings and cars have architectures, and so do complex programs. A program's software architecture is its pattern of module assembly. The box-and-arrows diagrams seen in this chapter are simple depictions of software architecture. Although it is too soon for us to make intensive study of software architecture, we can benefit from a survey of several forms of software architecture that have proved useful in practice.
In the examples that follow, we use diagrams to display an architecture. The boxes represent modules, a solid line denotes the importation of one module by another, and a dotted line denotes the transfer of information from one module to another, typically as answers returned by function calls.
The modules in the diagram are usually program modules. A sequential architecture is used to divide a too-large program into manageable, clearly defined stages. Many traditional business-processing programs (e.g., accounting/payroll) are designed in this format. In contrast, programs that require repeated human interaction cannot be easily constructed in this style.
As described in the examples in this chapter, each module has clear-cut role: the model module contains the data structure that represents a physical object like a gameboard or spreadsheet, the controller contains the algorithm that solves the desired problem by interacting with the model, and the view accepts user input and displays information from the model, as directed by the controller.
The MVC-architecture is particularly useful for building programs that require graphical user interfaces (GUIs), because the complex coding for the GUI is kept separate from the algorithm and data structure that are needed to solve the problem. As a result, a developer can improve any one of the GUI, algorithm, or data structure without having to rewrite the entire program.
Each layer represents a degree of closeness to the hardware:
If you have written an assembly program, the function calls you include in your program are calls to utilities functions.
The hierarchical architecture is especially useful for safety-critical software (aircraft controllers, medical-device controllers) where program correctness is paramount. This is because the hierarchy makes it easy to design the large system top down (please review the section on top-down programming in the previous chapter) and then test and prove correctness bottom up.
Bottom-up correctness checking means that we begin testing and proving correctness of those modules that do not call functions in other modules. If needed, we can argue correctness with the additional knowledge of who the parent module must be. After the ``leaf'' modules are proved correct, we move to proving the correctness of the parent modules that use functions in the already proved modules. In this manner, we systematically test and prove correctness in careful, well defined stages.
There is a strong connection between this form of bottom-up correctness validation and inductive reasoning in mathematics.
The database acts as a central library of knowledge that all the processes can exploit. A standard example is a bank's database of customer accounts, which is shared by the bank's workers and ATMs.
The database architecture can also be used to design a form of problem solving where a family of ``experts,'' each of whom have a special skill, can work together to solve a complex problem. The problem is inserted into the database module, which is now called a blackboard, and the experts study the problem and each makes small steps towards its solution, leaving the results the results of the small steps in the blackboard module. Eventually, with the cooperation of all the experts, the problem is solved.
Here is a trivial example: one expert knows how to add integers,
another knows how to subtract them, and a third knows how to
multiply. The blackboard holds the problem,
(3 + 2) * ((5 -1) * 4)
The experts take turns contributing
their solution steps until the problem is completely solved,
e.g.,
(3 + 2) * ((5 - 1) * 4)
=> (3 + 2) * (4 * 4), by the subtraction expert
=> (3 + 2) * 16, by the multiplication expert
=> 5 * 16, by the addition expert
=> 80, by the multiplication expert
Blackboard architectures are commonly used for language processing and
machine learning.
A client-server architecture is built to allow multiple
processes, called ``clients,'' to request information or services
from a ``server,'' where there are multiple servers available to
service such requests:
The client does not care which server handles its request,
and there is a
mediating module, called an object request broker (ORB)
or software bus, that connects a client to an appropriate
(e.g., least busy) server.
The client-server architecture adapts well to systems that are distributed on many computers, and it has been highly successful for network applications, such as Google, where thousands of web-browser clients contact Google and are routed by its ORB to one of its hundreds of servers.
The structure of the ORB is crucial to the success of the architecture, and CORBA (Common Object Request Broker Architecture) is a formal description of the functions that must be written for the ORB. Any company that wishes to build a ``standard'' client-server architecture writes its ORB to match the CORBA description. Having said that, we note that Microsoft has its own variant of client-server architecture called COM (now part of .NET), and there is a third popular competitor called Enterprise JavaBeans.
Each module knows some of its neighbor modules. If a module has a request (it acts as a client), it sends the request to its neighbors. When a neighbor module receives a request, its mini-ORB asks the module to service it. If the module itself cannot service the request, the mini-ORB forwards the request to the module's neighbors. In this way, a request travels through the system until it reaches a module that can service it and return a result.
Peer-to-peer networks are highly reliable in the sense that they operate even when modules appear, disappear, and fail. They are unreliable in that there is no guarantee that any particular request will be serviced promptly or at all. For this reason, the architecture is often used for distributed-library activities (e.g., sharing copies of bootlegged music files).
All these developments are made possible by the invention of the module --- a program unit that holds a data structure, some start-up commands, and some functions that are contacted by other modules.
FIGURE======================================================= """Clock models a clock that ticks and emits an alert every 60 ticks Operations: tick (increments the clock by one tick), bigTick (increments the clock by how_many times) getTime (returns the current time) """ time = 0 # Line 6 def tick() : # 8 global time # 9 time = time + 1 # 10 if (time % 60) == 0 : # 11 print "Alert:", time # 12 def bigTick(how_many) : # 14 for i in range(how_many) :# 15 tick() # 16 def getTime() : # 18 global time # 19 return time # 20 ENDFIGURE=========================================================
FIGURE====================================================== """Con tests the Clock module""" import Clock # Line 22 x = [ Clock.getTime() ] # 24 Clock.bigTick(2) # 25 x.append(Clock.time) # 26 ENDFIGURE=========================================================When the Con module is started, computer storage looks like this; there is an empty namespace for Con:
The i.c. variable remembers the next command to execute and the n.s. variables remembers which namespace to use with the command to execute.
The import Clock instruction constructs a namespace for
Clock and starts its commands ---
i.c. is set to 4 and n.s. is set
to Clock:
Variable time is constructed in Clock's namespace,
and the functions at Lines 6, 11, and, 15 are saved in Clock's
namespace.
At this point, importation is complete, and execution returns to Line 24
in Con:
Notice that Clock's namespace remains
and that
an entry for Clock appears in Con's namespace.
The assignment at Line 24 proceeds in the usual three steps: First,
Con's namespace is checked to see if x exists;
it doesn't, so a cell is constructed.
Next, the expression, Clock.getTime() must be calculated to its value.
First, Clock is found in the namespace: the name refers to the
Clock's namespace, so the latter is
searched for function
getTime. The function is found and is started
at Line 19 with its own private namespace:
The n.s. variable is reset, and within the namespace for
getTime, the global n.s. is correctly set
to Clock (and not Con).
Line 19 makes time
link to the variable held in the function's global namespace.
We see that return time calculates that time computes to
0 (which is the value of time in namespace Clock,
and the 0 is returned to the called of getTime. This
lets us build a list to hold the 0 and
finish the assignment to x at Line 24:
Line 25 calls Clock.bigTick, which
immediately calls tick:
Function tick executes,
the conditional in Lines 9 and 10 is executed (the condition evaluates to
False), and the function finishes.
Then, tick is called a second time, and a fresh copy
of its private namespace is built again.
The function computes a second time and finishes.
Line 26 computes the value of Clock.time and appends it to list
x.
The example shows that each module has its own, permanent namespace, and when we call a function, the function's ``global'' namespace is the one associated with the module in which the function is defined.
Finally, the example showed that it is possible to reference the
variables within a module by merely mentioning their names, e.g.,
Clock.time. But
That is, we should never try to do
Clock.time = Clock.time + 1
Remember that a key reason for breaking a program into modules is
to group together variables with the functions that maintain them.
No harm is done by looking at a variable in another module, but
changing the variable means that the ``maintenance''
is no longer done in just one module!
A module that has no functions that are called by other modules is a program module or program, for short.
One module links to another by importing it. The command forms are
A function defined in MODULE is referenced as MODULE.NAME(ARGUMENTS).
dir() # lists the bindings in the current namespace dir(M) # lists the bindings in the namespace of module, M print M.__doc__ # displays module M's documentation string