Look at the computer you use --- it was assembled from ``prefab'' pieces --- the display was built by one manufacturer, and it is bolted and wired to the ``motherboard'' and CPU, which were built by another. The case was built again by someone else, as was the disk drive and any other gadgets (printers, DVD drives, cameras) that are attached.
The idea of assembling a product from parts that fit together is not new. It was first devised by firearms manufacturers in the eighteenth century. Gunmakers found it impossible to build and service all the guns wanted by hunters (and, alas, soldiers). To cope, the gunmakers designed standard-sized parts that fitted together, like a jigsaw puzzle does, into a gun. This way, more guns could be built, and when a part on the gun broke, it could be replaced by a new, duplicate part.
The idea of standard parts was used by Henry Ford to develop the modern assembly line, where entire automobiles were built from standard-sized parts, connected in a standard way. Almost all products we buy today (even food!) are built using assembly-line techniques.
Assembly-line ideas also apply to computer programs: all large computer programs are assembled from parts (``components'') that connect together in standard ways. Since programs look like recipes in an odd mathematical language, it is not immediately clear what it means to build a recipe from ``components.'' But if you think about how a book is built from a sequence of chapters, and how a person who first appears in one chapter reappears in a later chapter, then you are getting the idea --- the characters and settings are ``parts'' that are ``assembled'' into a plot. An author rarely writes a book from Chapter First to Chapter Last with all the characters, setting, and plot completely fixed in place, and large computer programs are rarely written by one person going from the first command to the last --- both books and programs are built from parts that connect together.
There are two standard ways of building ``parts'' of computer programs that connect together: functions and modules. Stated crudely, functions are ``small parts'' (like nuts and bolts) that are used in lots of places, and modules are ``big parts'' (like engine and transmission assemblies) that must be bolted together to make the final product. In this chapter, we learn how to manufacture the ``small parts''; the next chapter tells us about the ``big parts.''
About fifty years of programming experience has proved that a human can understand and keep in mind about 50 lines of computer code at one time --- just 50 lines! This is not enough to build any real program. How can we develop programs that are longer? In addition to writing good comments for the code, the solution lies in writing the program in pieces (of no more than 50 lines each) and connecting them together, much like a book is written in chapters.
The function is the best-known construction for building program pieces. (Functions are also called ``subroutines,'' ``procedures,'' and ``methods.'' There are some nuances in using these terms, but they don't matter here.) Simply stated, a function is a sequence of commands bundled up with a name, and when we later mention the name, the bundled-up commands execute. (It is like when your instructor barks, ``Read Chapter 10!'' You ``execute'' Chapter 10.)
In your Python program, you are already using functions that were written by the Python designers. Let's review a few:
The EXPRESSION is called the function's argument --- It is extra information that the function requires to do its job, here, of printing something on the display.
We use the function in examples like, print "Your score is :", score.
We use the function in examples like, name = raw_input("Please type your name: ")
We use the function in examples like, age = int(raw_input("Please type your age: ")). This example also uses the raw_input function, whose answer becomes the argument for the int function.
Just like there are different forms of parts for building a TV or car, there are different forms of functions:
To write a function in Python, we state its name
and we write its commands, indented, underneath. Here is a first example:
# error prints a generic error message.
def error() :
text = "ErrorErrorErrorErrorErrorErrorError\n"
print text + text + text
The function is named error, and the keyword, def,
tells us (and the Python interpreter) that this line defines
a new function. The matching parentheses, ()
state that the function requires no argument. Note the colon afterwards.
(The space between the right parenthesis and the colon is optional,
and most people omit it.)
The function's commands (its body) are the two indented commands underneath.
If you wish, you may pretend that
Figure 1 shows a sample program that will manage cash withdrawals
for an ATM (automated teller machine);
the program asks the user to type a dollars-and-cents amount,
and if the amount is
in error, then the error function is executed by the main program:
FIGURE 1=================================================
# ATM1 lets a human withdraw cash from an automated teller machine.
# error prints a generic error message.
def error():
text = "ErrorErrorErrorErrorErrorErrorError\n"
print text + text + text
# The ``main program'' starts here:
dollars = int(raw_input("Type dollars amount: "))
if dollars < 0 :
error() # this calls (executes) the function
cents = int(raw_input("Type cents amount: "))
if (cents < 0) or (cents > 99) :
error() # this calls (executes) the function
# the step that does cash withdrawal goes here....
raw_input("\n\npress Enter to finish")
ENDFIGURE==========================================
The function is defined first, and the program follows. The
program executes (calls or invokes)
error() when the program receives
bad input data.
It is always a good idea to include a line or two of comments that explain what the function does. This reminds us later how we should use the function.
Let's test the program:
$ python ATM1.py
Type dollars amount: -4
ErrorErrorErrorErrorErrorErrorError
ErrorErrorErrorErrorErrorErrorError
ErrorErrorErrorErrorErrorErrorError
Type cents amount: 123
ErrorErrorErrorErrorErrorErrorError
ErrorErrorErrorErrorErrorErrorError
ErrorErrorErrorErrorErrorErrorError
Here is an explanation of what happened:
Remember:
Here are some diagrams that visualize these steps:
The program starts in this configuration:
and error's definition is saved for later use.
(Actually, the instruction number address of the start of error's body
is saved.)
After the input is read and is found to be negative,
function error is called, and its commands are executed just as if
they were copied into the position where the function was called:
The function's commands execute
while the program is paused, and they use a new, private namespace that is
constructed for the function's use:
Once the function finishes, execution resumes in the program:
The diagrams reveal that function error has its own, ``local'' namespace for its own, ''local'' variable, text. The function's namespace disappears when the function finishes.
It's true that function error does just a small task and
is not essential, but
compare the program in the Figure with the one that follows below,
without the function ---
The one below is a bit harder to understand because there are more details
in the way:
========================
dollars = int(raw_input("Type dollars amount: "))
if dollars < 0 :
text = "ErrorErrorErrorErrorErrorErrorError\n"
print text + text + text
cents = int(raw_input("Type cents amount: "))
if (cents < 0) or (cents > 99) :
text = "ErrorErrorErrorErrorErrorErrorError\n"
print text + text + text
=============================
Also,
if we decide to change the error message to something different,
then we must change the message in two places in the second program.
In the first program, we change just the function definition, say
like this:
def error() :
print "Error. Sorry.\n"
When we define ``helper'' functions like error for
doing such ``little things,'' we will find it easier
to customize or change
our program in the future.
The examples of the error-printing functions show us the first use of functions:
def error() : """error prints a generic error message.""" text = "ErrorErrorErrorErrorErrorErrorError\n" print text + text + textThe documentation string is indented just like the rest of the body of the function, and it begins and ends with three double-quotes. (For this reason, the documentation string can be multiple-lines long.)
The documentation string causes no harm when the function executes,
and in fact,
you can now print it, using the print command.
Try this:
print error.__doc__
This prints error's documentation string.
Also, there are some development-environment tools for Python that use the
documentation strings to generate on-line documentation pages for
Python programs. So, we will use the documentation strings in our
functions.
Most of Python's prewritten functions have documentation strings.
For example, try
print raw_input.__doc__
and
import random
print random.randrange.__doc__
Again, here is the error function:
def error() :
"""error prints a generic error message."""
text = "ErrorErrorErrorErrorErrorErrorError\n"
print text + text + text
We can test the function by itself, interactively, like this:
$python -i Test.py >>>This tells the Python interpreter to read Test.py, even though it is an incomplete program. Notice the -i --- this states that, after the program is read, the Python interpreter allows you to use the functions and variables in Test.py just as if you had typed them by hand.
>>> error()This executes the function, just as if it was called from within a program. You will see printed, as expected,
ErrorErrorErrorErrorErrorErrorError ErrorErrorErrorErrorErrorErrorError ErrorErrorErrorErrorErrorErrorErrorAnd, you can read its documentation string:
>>> print error.__doc__ 'error prints a generic error message.'
def error() : """error prints a generic error message.""" print "Error. Sorry.\n"We save this change in a text editor, we stop the Python interpreter (type exit), and we restart it:
$python -i Test.py >>>Now we can test the revised function:
>>> error() Error. Sorry.
message = raw_input("Please type something: ") ... compute with the message ...Functions might also require arguments, but they do not receive them via raw_input. Instead, the arguments are given to the function by the program that calls it. The argument enters the function via a new variable, called a parameter. Here is one example:
=============== def errorMessage(message) : """errorMessage prints a specific error message. parameter: message - a string, stating the error """ text = "Error: " + message print text + " Sorry.\n" ===================The definition line begins with def, followed by the function's name, followed by a variable name in parentheses, and then the colon. (The documentation string will be examined in a moment.) Here, message is a Python variable, just like variable text. It is called a parameter (or formal parameter), and it ``connects'' to an argument, somewhat like this:
message = ... the argument that arrives when the function is called ... text = "Error: " + message print text + "\nSorry."The main program might call the function like this:
errorMessage("You typed a negative number.")This starts function errorMessage so that
message = "You typed a negative number."executes first, and then the commands
text = "Error: " + message print text + " Sorry.\n"execute next.
The documentation string summarizes what the function does and explains how the parameter will be used within the function. This information is helpful when others call the function. (Also, it is a standard Python style to leave a blank line between the first line of the documentation string, which explains what the function does, and the rest, which explains the parameter(s).)
In errorMessage("You typed a negative number."), the "You typed a negative number." part is called the argument. When the function is called, the argument is assigned to the parameter.
Let's test the function in an improved ATM program:
FIGURE 2=================================================
# ATM2 lets a human withdraw cash from an automated teller machine.
def errorMessage(message) :
"""errorMessage prints a specific error message.
parameter: message - a string, stating the error
"""
text = "Error: " + message
print text + " Sorry.\n"
# The program starts here:
dollars = int(raw_input("Type dollars amount: "))
if dollars < 0 :
errorMessage("You typed a negative number.")
cents = int(raw_input("Type cents amount: "))
if (cents < 0) or (cents > 99) :
errorMessage("The cents amount was not in the range 0..99.")
# the step that does cash withdrawal goes here ...
raw_input("\n\npress Enter to finish")
ENDFIGURE===========================================
Here is the test:
$ python ATM2.py
Type dollars amount: -5
Error: You typed a negative number. Sorry.
Type cents amount: 123
Error: The cents amount was not in the range 0..99. Sorry.
The program shows that the function can be called with distinct arguments,
and each call prints a different message,
because of the distinct arguments.
Here is the execution of the test program, drawn in pictures, showing how arguments and parameters
are used:
The program starts and the function is saved:
The input is read, saved, and the first condition is True:
This calls errorMessage.
The execution proceeds just like the function's
body is copied into the position of the function call with the
assignment, message = "negative":
The parameter is saved in the function's namespace, and
the function's commands execute:
When the function finishes, the program resumes:
Look again at the function's comment:
def errorMessage(message) :
"""errorMessage prints a specific error message.
parameter: message - a string, stating the error
"""
text = "Error: " + message
print text + " Sorry.\n"
The third line of the documentation string explains
that the
argument to be assigned to the parameter
must be a string. Notice, for example, that this call
causes a stoppage of the program:
errorMessage(99)
A function's argument is its ``assumed input,'' and the function cannot
guarantee its work if the argument is inappropriate for the parameter.
A function can be called with an argument that is a compound expression,
like this:
data = raw_input("Type cents amount: ")
cents = int(data)
if (cents < 0) or (cents > 99) :
errorMessage("The input, " + data + ", is incorrect.")
Here, the expression, "The input, " + data + ", is incorrect."
is computed to its answer, a string, before function errorMessage
starts its work. For example, if the user types 123
as the input for variable data, then
the argument to the function computes to
"The input, 123, is incorrect." and only then does the function
start (by assigning the string to parameter message).
def errorMessage(message) : """errorMessage prints a specific error message. parameter: message - a string, stating the error """ text = "Error: " + message print text + " Sorry.\n"in a file, Test.py, and test it:
$python -i Test.py >>>
>>> errorMessage("too bad.") Error: too bad. Sorry. >>> errorMessage("R2D2 " + "is lost!") Error: R2D2 is lost! Sorry. >>> errorMessage(13) Traceback (most recent call last): File "Repeated tests are necessary to verify that the arguments are used correctly by the function.", line 1, in ? File "Test.py", line 12, in errorMessage text = "Error: " + message TypeError: cannot concatenate 'str' and 'int' objects >>> print errorMessage.__doc__ errorMessage prints a specific error message. parameter: message - a string, stating the error
====================== def error() : """error prints a generic error message.""" text = "ErrorErrorErrorErrorErrorErrorError\n" print text + text + text ======================We might write another function that calls it:
==================== def printBigError(message) : """printBigError prints an error message surrounded by an annoying border parameter: message - a string, containing the error message """ error() print message error() ============================When we call the new function, say, with
printBigError("I am so totally lost!")We get this printout:
ErrorErrorErrorErrorErrorErrorError ErrorErrorErrorErrorErrorErrorError ErrorErrorErrorErrorErrorErrorError I am so totally lost! ErrorErrorErrorErrorErrorErrorError ErrorErrorErrorErrorErrorErrorError ErrorErrorErrorErrorErrorErrorErrorHere are diagrams that show what happened. We begin with this configuration:
the function assigns the argument to parameter message, as usual. Then, execution within printBigError is paused, because a second function, error() is called:
Once the commands within error finish, then execution can resume with print message:
The second call to error executes next:
and finally all the called functions finish:
It is OK for a function to have more than one parameter.
Here is an example:
def printAlert(priority_level, time, message) :
"""printAlert prints an alert message
parameters: priority_level - an int, stating the level of the alert
time - a string, stating the time the alert was issued
message - a string, stating the warning
"""
print "Alert! Priority", priority_level
print time + " - " + message
There are three parameters: an integer followed by two strings.
When the function is called, there must be three arguments that
match the parameters:
printAlert(1, "12:01p.m.", "it's lunchtime!")
The three arguments are assigned, one by one,
to the parameters, like this:
priority_level = 1
time = "12:01p.m."
message = "it's lunchtime!"
and the rest of the commands execute, printing,
Alert! Priority 1
12:01p.m. - it's lunchtime!
printAlert(time = "12:01p.m.", message = "it's lunchtime!", priority_level = 1)The order of the arguments no longer matters.
message = raw_input("Type something: ") ... compute an answer from the message ... print answerFunctions sometimes compute answers, too, but they do not always print them. Instead, they return them to the place in the program that called the function.
We have used this form of function before: Python's built-in functions, raw_input and int, return answers. We make our functions return an answer by using the Python command, return.
Here is a small function that computes and returns the reciprocal
of its argument:
def reciprocal(n) :
"""reciprocal computes the reciprocal of its argument
parameter: n - an int, which must not equal 0
returns: the float, 1.0/n (When n==0, 0.0 is returned.)
"""
answer = 0.0
if n != 0 :
answer = (1.0 / n)
return answer
The last line,
return answer, commands that the number named by answer
is inserted at exactly the position where the function was called.
For example, if the calling command was this:
one_third = reciprocal(3)
then the answer, 0.33333333333333331 ``replaces'' the function's call
and is assigned to variable
one_third. Or, if the calling command was this:
print "1/3 is", reciprocal(3)
then the answer ``replaces'' the function's call and we see printed,
1/3 is 0.33333333333333331
Here is a second example, where we write a function to do something slightly complicated and important. (We will use the function in the ATM program were are building.)
Accounting programs maintain balances in cents-only format
(for example, $30.95 is stored as 3095), because
bankers hate to lose fractions of pennies due to fractional-arithmetic
imprecision. But when we print a cents-only amount,
we must reformat it as a dollars-cents string (so that 3095
prints as "30.95"). Here is a function that
does the reformatting:
============================
def formatDollarsCents(amount) :
"""formatDollarsCents formats a cents amount into a dollars.cents string
parameter: amount - a nonegative int, a cents-only amount
returns: a string, formatted as dollars.cents.
(If amount is < 0, then the string, 'negative' is returned.)
"""
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
=================================
answer receives a string that correctly formats the
argument in dollars-cents format. The string is
returned as the answer that is inserted at exactly
the position where the function was called.
Say that we test the function with this:
current_balance = 3205 # 32 dollars and 5 cents
print "Balance is " + formatDollarsCents(current_balance)
we see
Balance is $32.05
printed. This is because
"Balance is " + formatDollarsCents(current_balance)To compute this, formatDollarsCents(current_balance) is invoked.
Execution pauses in the middle of the expression so that formatDollarsCents can compute and return an answer:
The formatting function will will be used within the ATM program. The function contained some delicate computation steps, and it was good that we wrote and tested the function seperately. Indeed, this is a second standard use of functions:
Here is the latest version of the ATM program; it uses the function
we just wrote to print the amount the user wishes to withdraw:
FIGURE===================================================
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
returns: a string, formatted as dollars.cents.
(If amount is < 0, then the string, 'negative' is returned.)
"""
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:
OK = True
dollars = int(raw_input("Type dollars amount: "))
if dollars < 0 :
errorMessage("You typed a negative number.")
OK = False
if OK :
cents = int(raw_input("Type cents amount: "))
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 "Amount to withdraw is " + formatDollarsCents(request)
# ... subtract request from the account's balance
# and emit cash ...
raw_input("\n\npress Enter to finish")
ENDFIGURE=================================================
The restrictions on a functions argument that make the argument appropriate are called preconditions, and the form of answer that is computed and returned is called a postcondition. Often the precondition and postcondition are stated in informal English, but if we wish to formalize the algebraic knowledge produced by a function, we can write pre- and postconditions in an algebraic way.
For example, here is the reciprocal function from the previous
section, where its documentation string is augmented with more precisely
stated pre- and postcondition information:
================================================
def reciprocal(n) :
"""reciprocal computes the reciprocal of its argument
parameter: n - an int, which must not equal 0
precondition: n != 0
returns: the float, 1.0/n (When n==0, 0.0 is returned.)
postcondition: answer = 1.0/n
"""
answer = 0.0
if n != 0 :
answer = (1.0 / n)
return answer
======================================================
The precodition lists an algebraic condition under which the function
will behave properly, and the postcondition lists the algebraic property
of the function's answer (named, ``answer'') that holds true,
provided that the function's arguments made the precondition
hold true.
Let's look more closely at this relationship, that is, how a precondition
can be used to calculate how a function behaves and calculates
the knowledge asserted in the postcondition. We use the usual
algebraic reasoning on the function's body:
# assert the precondition: n != 0
answer = 0.0
# assert: n != 0 and answer = 0.0
# implies: n != 0, meaning that the test the follows must equal True
if n != 0 :
answer = (1.0 / n)
# assert: n != 0 and answer = 1.0 / n
# assert: n != 0 and answer = 1.0 / n
return answer
# assert the postcondition: answer = 1.0 / n
With the precondition, we guarantee that the
if-command will compute its test to True and the division
will take place. This ensures the truth of the postcondition.
A function's pre- and postconditions are used to do logical reasoning
when the function is called. For example, the main program might
invoke reciprocal. We can reason about the knowledge that is
produced by the function call, like this:
# In the main program:
x = ...
# assert: x > 2
# implies: x != 0 (this makes true reciprocal's precondition)
y = reciprocal(x)
# assert: y = 1.0/x (because reciprocal's postcondition must be true)
Since we proved that the precondition is make true by the
argument, x, we can assert the postcondition for
variable y, which receives the result of the invocation.
Here is a more precise statement of the reasoning principle for
function calls. For a
function,
def f(params)
"""precondition: PRE(params)
postcodition: POST(answer)
"""
COMMANDs
and an invocation of it, x = f(args),
x = ... # assert: x > 2 # implies: x != 0 # Since reciprocal's precondition is n != 0, # and since x is the argument for parameter n, we must show that # x != 0 to show that we are calling reciprocal correctly. # This is true here. y = reciprocal(x) # reciprocal's postcondition is answer == 1.0/n, # and since the answer is assigned to y, we have that # y == 1.0/xSince we proved that the precondition is true for argument, x, we asserted the postcondition for variable y.
Of course, the computer will let us call reciprocal incorrectly,
like this:
x = 0
y = reciprocal(x)
but no useful knowledge is produced from the call,
because the function's
precondition is False. If it is crucial that the function is
computed only when its precondition is True, then we might use a
Python assert command to enforce the precondition.
This might be done as follows:
================================================
def reciprocal(n) :
"""reciprocal computes the reciprocal of its argument
parameter: n - an int, which must not equal 0
precondition: n != 0
returns: the float, 1.0/n (When n==0, 0.0 is returned.)
postcondition: answer == 1.0/n
"""
assert n != 0 # check the precondition before proceeding
answer = (1.0 / n)
return answer
======================================================
Here is a second example where pre- and postconditions help
us understand precisely what a function accomplishes:
The formatting function for monetary amounts might have these
pre- and postconditions:
=======================================================
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 postcondition relates the structure of the
answer string to the function's parameter.
This knowledge ensures users of the function that the function computes
correct information for printing, like this:
money = ...
# assert: money >= 0
print formatDollarsCents(money)
# assert: "$D.C" printed, where (D*100) + C == money
Using preconditions and postconditions, we can write precise algebraic properties of functions and insert these properties into the programs that call the functions. In this way, we can continue to calculate the precise knowledge calculated by the main program that calls the functions.
We can understand global variables with a simple example: Say that
a program uses a ``clock,'' and
every sixty clock ticks, an alert must sound.
The program makes a variable for the clock and starts it at zero:
clock = 0
Within the program, there are places where the clock should tick --- say,
after every third instruction. It is best to write a helper function,
tick(), to make the clock tick by 1. The function is called
within the program somewhat like this:
... # command 1
... # command 2
... # command 3
tick()
... # command 4
... # command 5
... # command 6
tick()
... # and so on
Let's write the function. If we try to write it like this:
def tick() : # this version does not work correctly; read below....
clock = clock + 1
if (clock % 60) == 0 : # time to sound an alert ?
print "\a", "Alert: time is", clock
then, the first time we execute tick(), we see
this Python error message:
>>> tick()
Traceback (most recent call last):
File "
The message tries to tell us tick is confused about
variable clock, because the variable is not inside (``local to'')
the function.
Here is the repair: When a function uses a variable that is not internal to its body, the function must assert that the variable is found elsewhere --- the variable is global.
Here is the correct coding and usage of function tick, which
increments the ``global variable,'' clock:
FIGURE============================
# TestClock tests a function that uses a global variable.
# Here is the ``global variable'':
clock = 0
def tick() :
"""tick makes the clock increase in time by 1 tick.
Every 60 ticks, an alert is sounded and printed.
"""
global clock # asserts that variable clock is found outside the function
clock = clock + 1
if (clock % 60) == 0 : # time to sound an alert ?
print "\a", "Alert: time is", clock # "\a" rings the keyboard bell
# The program starts here: After every print command, the clock ``ticks'',
# and then the program pauses (``sleeps'') for 0.1 seconds:
# Important: To stop this program, press both Ctrl and c
import time
while True :
print " "
tick() # it's time to move forward the clock!
time.sleep(0.1) # pause 0.1 seconds
ENDFIGURE==============================================
Look at the first command in the body of tick:
global clock tells the Python interpreter where to find
the variable that must be incremented.
When the function is called and the global clock
command is executed,
a notation is placed in tick's namespace that variable
clock is found in the ``global'' namespace.
When the following assignment, clock = clock + 1
executes, the notation in tick's namespace directs the
computer to find the value of clock in the global namespace.
The assignment to clock is done to the variable in the
global namespace, and the function finishes:
The ATM program we are developing should consult the customer's bank account to learn the customer's balance because conducting any withdrawal.
In reality, the customer' bank account is kept in a separate database
program at the bank. Such a database will be a data structure --- like
a list --- of customers' balances. As a simple example,
say that each customer has a bank-account number that is a nonnegative
int, such as 0 or 1 or 2 .... Then, we can keep the balances for
the bank's customers in a list. For example,
if the bank has three customers, where
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,
then the list of accounts might be defined as
all_accounts = [10050, 3000, 25066]
where the amounts are stored in pennies.
If a customer wishes to withdraw money from their account,
the customer's account in the list must be consulted, and the
amount withdrawn must be subtracted from the account's balance.
It is best to write functions that get a customer's balance
and that do withdrawals.
This is because the list of accounts is a critical piece of information
(to the bank!) and it is best to use maintenance functions that
correctly maintains the list.
========
# 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 is a sample database to get started.
# (Usually, this information is read from a disk file.)
all_accounts = [10050, 3000, 25066]
#### Maintenance functions for the database:
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 the account info:
all_accounts[account_num] = balance - amount_withdrawn
return amount_withdrawn
========
These two functions ``maintain'' the bank-account.
Since the two functions share the list, all_accounts,
it is global to both.
For example, if an ATM wishes to learn the balance for account 2,
it would call the function, getBalance, like this:
balance = getBalance(2)
and a withdrawal of $10 from the same account would be done by this
function call:
cash_withdrawn = withdraw(2, 1000)
Here is the final version of the ATM program with all its functions in
place. It uses the simulated bank database seen above.
FIGURE===================================================
# ATM lets a human withdraw cash from an automated teller machine.
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 = 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 = getBalance(account)
print "Balance is", formatDollarsCents(new_balance)
raw_input("\nTransaction completed")
ENDFIGURE================================================
Of course, a real-life bank database would be a huge data structure of bank accounts that live in a separate program on a separate computer. The ATM program would communicate with the database to obtain balance and withdrawal information. In such a case, the functions are used by the ATM program to communicate with the database. Even though our ATM example is simplistic, it is faithful to a real-life ATM and bank database. In the next chapter, we learn how to write programs in separate files that can communicate with each other by means of functions.
In the previous section, we modelled a
bank account that might be used with an ATM. The example is important and
we repeat it here, along with some additional functions that
let a bank officer add new accounts and make deposits.
==================================================
# 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:
# A banker uses these functions to create new accounts and deposit money:
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, for the newly created account
"""
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
=========================================================
The functions, withdraw and getBalance, do the maintenance and are carefully written so that the value of balance is never altered incorrectly (e.g., a negative value for balance never arises). People and ATMs that wish to examine and alter the account's balance use the maintenance functions to do this delicate work --- they do not assign to balance directly.
The above coding would almost certainly live in a separate file on a separate computer; the bank's employees and the ATM's would be situated on other computers and use their own programs to call the withdraw and getBalance functions. (We learn how to make this arrangement in the next Chapter.)
Say that our company must build and maintain an electronic telephone directory that is shared by its employees. For design reasons and for practical reasons, we build the telephone directory by itself: We choose a data structure for the telephone directory and functions that will correctly maintain the directory.
Perhaps the directory looks like the one from the previous chapter,
so we use a dictionary data structure, where information
is stored like this:
tel_book = {"Jane Sprat": 4098, "Jake Jones": 4139, "Lana Lang": 2135}
The company's employees wish to (i) look up phone numbers, (ii) insert new people and their phone numbers, and (iii) delete people who quit the company. Since there are three actions one can do with the telephone book (dictionary), we write three maintenance functions, one for each action.
Each function is simple to write, and we quickly design
and code the computerized telephone book:
========================================================
# The telephone book: It maps a person's name to their phone number,
# e.g., tel_book = {"Jane Sprat": 4098, "Jake Jones": 4139, "Lana Lang": 2135}
tel_book = { }
# 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
precondition: name in tel_book
returns: the phone number for name. If name not in the book, -1 returned
postcondition: number == tel_book[name]
"""
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
precondition: name not in tel_book
postcondition: tel_book[name] == 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.
precondition: name in tel_book
postcondition: name not in tel_book
"""
global tel_book
if name in tel_book :
del tel_book[name]
=================================================================
Next, we should place this coding in a file and interactively test
the functions, one at a time. The test session might go like this:
$python -i Phone.py
>>> print tel_book
{}
>>> insert("ed", 3456)
>>> print tel_book
{'ed': 3456}
>>> insert("maria", 1111)
>>> print tel_book
{'ed': 3456, 'maria': 1111}
>>> lookup("maria")
1111
and so on. Notice that, in between our function testing,
we can interactively peek inside the
global variable and see its value.
This is useful for checking that
the variable is correctly updated by the functions.
We are allowed to do this
because we are the software developers, and we want to see all the
details within the program we build. But the people who will
use our telephone book will not do this --- they will use the
functions as the ``entry points'' into the telephone book.
Last chapter we saw how to use a grid to model a slide puzzle,
which is a simple example of a physical game board.
We can extract the modelling of the puzzle and
its function that slides puzzle's pieces.
Here is the data structure and its
maintenance functions:
==========================================================
# Models a slide puzzle holding positive 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 slides 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 printPuzzle() :
"""printPuzzle displays the puzzle's contents"""
global puzzle
import string
for row in puzzle :
for item in row :
if item == 0 :
print " ",
else :
print string.zfill(item, 2), # print a two-symbol number
print
=============================================================
The difficult details about sliding a piece into the puzzle's empty
space are all contained within function move. Simularly,
the details of formatting of the puzzle for printing are contained
within printPuzzle. This makes it truly easy to write
programs that play with the puzzle:
===========================
while True :
printPuzzle()
num = int(raw_input("Type number of piece to move: "))
OK = move(num)
if not OK :
print "Illegal move --- try again"
================================
In the next chapter, we will learn how to package a global variable and its helper functions into a separate file --- a module --- which is a ``big part'' or ``subassembly'' that can be connected together to other files to assemble a big program.
Here is a small example:
=========================
def summation(v) :
"""summation computes the sum of a list of integers
parameter: v - a list of ints
returns: the sum, v[0] + v[1] + ... + v[len(v)-1]
"""
total = 0
for value in v :
total = total + value
return total
# Test the function:
a = [0, 1, 2, 3, 4]
print summation(a)
raw_input("\n\npress Enter to finish")
==============================
For example, the slide puzzle program used a grid to represent
the puzzle. The key data invariant for the puzzle is this:
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
That is, no matter how often puzzle is updated, its cells hold
numbers that are exactly some permutation (mixed-up set of) 0 to 15.
A data structure's maintenance functions must maintain the data structure's data invariant. Since the maintenance functions are the only functions that have permission to directly change the data structure, we should always carefully check, when each maintenance function is called and finishes its work, that the data invariant still holds true.
For the slide puzzle in the previous section, it is clear that the
printPuzzle function does not alter the puzzle, so it
maintains the puzzle's data invariant. When we study the
move function, we notice that it alters the puzzle here:
puzzle[empty_space[0]][empty_space[1]] = num
puzzle[i][j] = 0 # the new empty space
Because we know that empty_space is a pair of two
numbers that remembers the coordinates of where 0 lives in the
puzzle, we can confirm that puzzle's data invariant is
kept true by these two commands, which exchange a 0 in one cell
with an integer in another cell.
Data invariants are critical to banking applications.
Consider one more time the bank-account example and note carefully
the data invariant attached to the account's balance:
========================================
# A modelling of one person's bank account.
import random
balance = random.randrange(100000) # the current balance, in cents
# data invariant: balance >= 0
# Maintenance functions for the account:
def withdraw(cash) :
"""withdraw removes cash from balance. If cash is greater than balance,
then only the remaining balance is withdrawn.
parameter - cash, an int, a cents-only amount
precondition: cash >= 0
returns the amount of cash withdrawn from the balance
postcondition: 0 <= amount_withdrawn <= cash
and amount_withdrawn + balance_new == balance_old
"""
global balance
if cash > balance :
amount_withdrawn = balance
else :
amount_withdrawn = cash
balance = balance - amount_withdrawn
return amount_withdrawn
def getBalance() :
"""getBalance returns the account's balance
returns the balance, in cents
"""
global balance
return balance
=========================================================
This account is never allowed to go negative, and a careful
analysis of the algebraic knowledge computed by the withdraw
function confirms that the data invariant is upheld:
global balance
# assert data invariant: balance >= 0
if cash > balance :
# assert: cash > balance
amount_withdrawn = balance
# assert: amount_withdrawn = balance
# implies: amount_withdrawn <= balance
else :
# assert: not(cash > balance)
# implies: cash <= balance
amount_withdrawn = cash
# assert: amount_withdrawn <= balance
# assert in both cases: amount_withdrawn <= balance
balance = balance - amount_withdrawn
# assert: balance >= 0
return amount_withdrawn
This reasoning is absolutely critical to the bank, who does not
wish to give away more money that what has been deposited!
Because we allow only the maintenance functions to alter the data structures, we need check the data invariants only within the maintenance functions. This greatly reduces the work we must do to ensure that the entire program, which might be truly huge, operates correctly.
All three reasons for coding functions will be used in big programs, and we must look for opportunities to employ them. Most programmers find bottom-up programming is easy to learn (you spot duplicate code or you find yourself copying-and-pasting code so you write a function that holds the duplicate code); modular programming is a bit tougher to learn (you look at how the program alters the main data structure and you write a function containing the commands that do the alterations); and top-down programming is the toughest to do well (you think about what are the ``big steps'' the program must do to solve the problem and you define a function for each big step).
We now do a case study to illustrate how these techniques might be used.
The program we wrote (Chapter 3, Figure 3?), was acceptable, but if we reread it, we see that there are too many little details in the main program, the same details appear in multiple places, and many of the commands do tiny steps on the ledger data structure. Now that we know about functions, we can redesign the program and do better. (By the way, many useful programs are written twice --- once to understand how to solve the problem, and once to build a good solution!)
Let's progress through the usual stages of program design and implementation.
$ python Checkbook2.py Type request: d(eposit), c(heck), v(iew), q(uit): d Type transaction amount (ddd.cc): $200.00 Type date of deposit: April 1 Type request: d(eposit), c(heck), v(iew), q(uit): c Type transaction amount (ddd.cc): $52.69 Type date and to whom check is payable: April 3 BookStore Co. Type request: d(eposit), c(heck), v(iew), q(uit): v deposit 200.00 April 1 check 52.69 April 3 BookStore Co. Current balance is: $147.31 Type request: d(eposit), c(heck), v(iew), q(uit): q Have a nice day.
The diagram suggests that the ledger is a sequence of transactions. We know that either a tuple or a list can be used to make a sequence; for now, we reuse the tuple from Chapter 3, because once a transaction is constructed and added to the ledger, there is no need to index it and alter its contents.
As for the transactions,
each transaction might be a single string, or we might
make each transaction into a tuple of its own, so that the
ledger looks like a tuple of tuples:
ledger = ( (20000, "deposit", "April 1"),
(5269, "check", "April 3 BookStore Co.") )
Let's do this.
The earlier sections in this chapter suggest that it will be useful to write some functions that maintain the balance and ledger. We will do this, but perhaps it is a bit too soon to know exactly what these functions might be. (We might guess that we will need a function that adds a new transaction to the ledger, and a function that updates the current balance. But what parameters should they use? What kind of answers should they return? Maybe it is too soon to know!)
In a situation where we are unsure which functions to write, we can continue to the next stage of development and later return to writing functions that update the data structure.
# here are the data structures: balance = 0 # the account's balance starts at 0 cents ledger = () # the history of transactions starts empty # here is the algorithm: processing = True while processing : read the transaction if transaction == "q" : processing = False elif transaction == "c" : process a check-writing request elif the transaction == "d" process a deposit elif the transaction == "v" : print the contents of the ledger and the balance else : the transaction is bad, so print an error messageNotice something important: phrases like ``process a check-writing request'' and ``process a deposit'' and ``print the contents of the ledger and the balance'' are describing important computation steps that we will probably want to design, write, and test one at a time. This suggests we might make each of these steps into a function:
processing = True while processing : read the transaction if transaction == "q" : processing = False elif transaction == "c" : processCheck() elif the transaction == "d" processDeposit() elif the transaction == "v" : printLedger() else : errorMessage("I don't understand your request; please try again.")Sometimes we keep all these function calls; sometimes we don't --- it often depends on whether it takes a lot of work or only a little to complete a step.
But let's get to work and
refine the step, process a deposit (processDeposit()):
Here's how we did it
in Chapter 3:
read the amount of the deposit
read the date of the deposit
add the amount of the deposit to the current balance
append the deposit info to the end of the ledger
When we refine the above into Python code, we are confronted with lots
of details:
# read the amount of the deposit:
data = raw_input("Type transaction amount (ddd.cc): $")
dollars_cents = data.split(".")
dollars = int(dollars_cents[0])
cents = int(dollars_cents[1])
amount = (dollars * 100) + cents
# read the date of the deposit:
info = raw_input("Type date of deposit: ")
# add the amount of the deposit to the current balance:
balance = balance + amount
# append the deposit info to the end of the ledger:
... whew! even more commands to write!
Clearly, we should bundle these steps into a function that we
write and test separately.
Even so, there are still too many details in the deposit step. First, a function that holds the commands that read the deposit amount would be helpful --- we will need a similar function for the check-writing step, anyway. Also, we see a command that is altering the global variable, balance, and there will be commands that alter the global variable, ledger. These commands are good candidates for bundling into functions that maintain the global data structures.
Before we invent these functions, let's quickly compare
the refinement of the deposit step
to the refinement
of the check-writing step:
read the amount of the check
read the date and payee
subtract the amount of the check from the current balance
append the check info to the end of the ledger
We see that the same commands will be needed
to read the amount of the check as the ones used for reading the
amount of the deposit! It is dead clear that we should define a function
for reading a dollars-cents amount.
We also see that the last step, that of appending the transaction to the ledger, is a repeat of the last step in the deposit transaction. So, a function should be written for this, also.
Here are the two functions that will be called by both the deposit
and check-writing steps:
def readAmount() :
"""readAmount reads a dollars.cents amount from the user and converts it
returns: the dollars.cents amount converted into cents-only, an int
"""
data = raw_input("Type transaction amount (ddd.cc): $")
dollars_cents = data.split(".")
dollars = int(dollars_cents[0])
cents = int(dollars_cents[1])
return (dollars * 100) + cents
def logTransaction(kind, amount, message) :
"""logTransaction adds a transaction to the end of the ledger.
parameters: kind - a string, either "deposit" or "check"
amount - an int, the amount of the transaction
message - a string, the details about the transaction
"""
pass # ...we must refine and write this one; for now, we use pass
The deposit step
now looks like this:
amount = readAmount()
info = raw_input("Type date of deposit:
# add the amount of the deposit to the current balance:
balance = balance + amount
logTransaction("deposit", amount, info)
This looks much better.
The coding for writing a check looks almost the same:
amount = readAmount()
info = raw_input("Type date and to whom check is payable: ")
balance = balance - amount
logTransaction("deposit", amount, info)
Now, it is time to apply ``modular programming'': When a data structure (or an important global variable), like balance or ledger, is altered inside the algorithm, we should seriously consider writing a function that holds the details about the alteration and place that function next to the data structure. This helps us see better how the data structure is constructed and maintained.
For the example,
we might define this function, which bundles together the changes
made by a deposit
to the global variables, balance and ledger:
def doDeposit(amount, info) :
"""doDeposit updates the balance and ledger with a deposit transaction.
parameters: amount - an int, the amount of the deposit
info - a string, the details of the transaction
"""
global balance
balance = balance + amount # add the deposit to the balance
logTransaction("deposit", amount, info)
The deposit step now looks like this; it's even simpler to read:
amount = readAmount()
info = raw_input("Type date of deposit: ")
doDeposit(amount, info)
In the same way,
we write a function for check-writing:
def doWithdrawal(amount, info) :
"""doWithdrawal updates the balance and ledger with a check-writing transaction
parameters: amount - an int, the amount of the check
info - a string, the details of the transaction
"""
global balance
balance = balance - amount # deduct the check from the balance
logTransaction("check", amount, info)
and the check-writing step looks like this:
amount = readAmount()
info = raw_input("Type date and to whom check is payable: ")
doWithdrawal(amount, info)
This looks pretty, because the function calls are explaining
to the human reader the steps required to process a check.
(Some people call such readable commands, self documenting code ---
there is no need for extra comments to explain what the program is doing.)
FIGURE========================================================= # Checkbook # maintains a ledger of checkbook transactions and a running balance # assumed input: a series of transactions of four forms: # (i) Deposits # (ii) Check written # (iii) View ledger: # (iv) Quit: # # guaranted output: a correct ledger, listing the sequence of deposits # and checks, along with the final balance, generated by the v command # The program's data structures (global variables) and the functions that # maintain them are listed here first: ledger = () # log of transactions # data invariant: remembers all user-supplied transactions balance = 0 # maintains the balance in cents def doWithdrawal(amount, info) : """doWithdrawal updates the balance and ledger with a check transaction. parameters: amount - an int, the amount of the check info - a string, the details of the transaction """ global balance balance = balance - amount # deduct the check from the balance logTransaction("check", amount, info) def doDeposit(amount, info) : """doDeposit updates the balance and ledger with a deposit transaction. parameters: amount - an int, the amount of the deposit info - a string, the details of the transaction """ global balance balance = balance + amount # add the deposit to the balance logTransaction("deposit", amount, info) def logTransaction(kind, amount, message) : """logTransaction adds a transaction to the end of the ledger. parameters: kind - a string, either "deposit" or "check" amount - an int, the amount of the transaction message - a string, the details about the transaction """ global ledger formatted_amount = formatDollarsCents(amount) info = (kind, formatted_amount, message) ledger = ledger + (info,) def printLedger() : """printLedger prints the contents of the ledger and the current balance""" global ledger for transaction in ledger : print transaction print "Current balance is:" , print "$" + formatDollarsCents(balance) def formatDollarsCents(amount) : """formatDollarsCents formats a cents amount into a dollars.cents string parameter: amount - a nonegative int, a cents-only amount returns: a string, formatted as dollars.cents. (If amount is < 0, then the string, 'negative' is returned.) """ 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 # to be continued =========================================================
FIGURE (concl.)=========================================== # Here are the helper functions for the main program: def errorMessage(message) : """errorMessage prints an error message. parameter: message - a string, the text of the message """ print "Error: " + message print "Sorry." def readAmount() : """readAmount reads a dollars.cents amount from the user and converts it returns: the dollars.cents amount converted into cents-only, an int """ data = raw_input("Type transaction amount (ddd.cc): $") dollars_cents = data.split(".") dollars = int(dollars_cents[0]) cents = int(dollars_cents[1]) return (dollars * 100) + cents # The main program starts here: processing = True while processing : request = raw_input("\nType request: d(eposit), c(heck), v(iew), q(uit): ") if request == "q" : # Quit processing = False elif request == "c" : # Check written amount = readAmount() info = raw_input("Type date and to whom check is payable: ") doWithdrawal(amount, info) elif request == "d" : # Deposit amount = readAmount() info = raw_input("Type date of deposit: ") doDeposit(amount, info) elif request == "v" : # View all transactions printLedger() else : errorMessage("I don't understand your request; please try again.") raw_input("\nHave a nice day.") ENDFIGURE====================================================The program includes a function, formatDollarsCents, that we studied earlier in the chapter.
At this point, we should test the program. If you do this, try to ``break'' the program by typing silly input values and also by writing more checks than what you deposit. It is clear that the program must be improved before we give it to an accountant to use!
Just now, the program uses a ledger data-structure that is
a tuple of transactions, where each transaction is itself a
tuple, e.g.,
ledger : ( ("deposit", 20000, "Apr 1"),
("check", 5769, "Apr 3, Bookstore Co.) )
This works fine, but say that we decide to implement
the ledger as a list, like this:
ledger : [ ("deposit", 20000, "Apr 1"),
("check", 5769, "Apr 3, Bookstore Co.) ]
Now, where must we change the program?
Because we used modular-programming techniques, we have one
function, logTransaction, which makes all the updates
to the ledger. This is the only function that must be changed in the
entire program! We ``unplug'' the existing logTransaction function
and replace it with this one:
def logTransaction(kind, amount, message) :
global ledger
formatted_amount = formatDollarsCents(amount)
info = (kind, formatted_amount, message)
ledger.append(info) # append adds a new cell to the end of a list
Then
we change the initialization assignment at the top of the program
to say
ledger = [ ]
and we are finished!
This little alteration show us how easy it is to alter and improve a function that is built from functions --- we unplug one function and replace it by its improvement.
Algorithm refinement is often a mix of bottom-up, top-down, and modular programming strategies. If doing all of them at once is overwhelming, then use the ``beginner's strategy'' to programming:
The final version of the checkbook accountant is well organized, and in particular, its main program is pretty and can be easily understood by a human. But the list of data structures and functions that precede the program is long and distracting.
In the next chapter, we will learn how to use Python modules to split the above program into two ``big pieces'': one piece will hold the data structures and their maintenance functions, and the other piece will hold the main program and its helper functions.
The previous case study extracted a collection of functions that talk about the actions we make with bank accounts: we read dollars-cents amounts, we make deposits and withdrawals, we print ledgers, and we format numbers for printing. These functions form a little ``language'' for talking about and working with checking accounts. There is something important going on here:
For example, a program that helps you play chess builds the world of chess in the computer; a program that does bank-account management builds the world of bank accounts in the computer; the slide-puzzle program built a slide puzzle inside the computer.
Each function used in the program is a ``little piece'' of code that makes an action. Each action is a significant concept, because we have bothered to name it!
For these two reasons, extracting and defining functions is not an idle, cosmetic task --- it is a central part of programming, because along with the data structures, the functions define the program's special world and give us the language to talk about how that world operates.
Many so-called domain-specific programming languages grew out of functions extracted from programs that were built to operate in a specific problem domain --- The family of functions became the basis of a new programming language!
After you complete this course, you will likely apply your computing skills in other courses. Say that you take a biology class, and you must write computer programs to do sequence matching of data to gene patterns. What is the ``language'' of genes and matching? You will uncover the answer by constructing your programs and naming the functions used in those programs.
At this point, you are ready to read Chapter 6 of Dawson's text. Study carefully his tic-tac-toe example, and try to deduce which of his functions were created from bottom-up, top-down, and modular programming styles. Think about whether the functions he chose define a language for talking about and playing tic-tac-toe.
In the earlier sections of this chapter, we encountered diagrams that showed what happens inside the computer when functions are called. The diagrams were helpful but informal. Now, we study a more precise description of what happens when functions are defined and called. Here is a quick review of the semantics of functions:
Here is a tricky example that we use to illustrate
functions, parameters, global variables, and
private namespaces:
FIGURE=========================================================
# program Test illustrates features of functions:
#Line:
a = 2 #3
def f(x, y) : #5
global a #6
a = g(x[0] * y) #7
def g(y) : #9
z = 6 #10
return y * z #11
# The program starts here:
b = [3,4] #14
f(b,4+1) #15
print a #16
ENDFIGURE============================================================
Read the program and guess what it prints.
When this program starts, computer storage looks like this:
The Python interpreter has an instruction counter (i.c.)
to remember the next command to execute, and it also has
a namespace variable (n.s.) to remember the namespace to use.
Storage is organized into a local-namespace area, and a global-namespace/heap
area.
After the command at Line 3 does its work, initializing a to 2, functions f and g are saved in Test's namespace. Only the initial instruction numbers of the functions are saved.
Next, Line 14 constructs a list in the heap and saves its address in
the variable cell for b:
Line 15 calls f. These steps are undertaken:
The Python interpreter has paused the execution at Line 15 and will execute the function at line 6. The namespace used is f's.
Line 6 adds a to the namespace, but a cell is not
created; instead, a is marked as belonging to
the global namespace (Test):
Line 7 starts an assignment to a.
First, since a is already present
in f's namespace, no cell is constructed.
Next, the expression, g(x[0] * y), must be computed, so that it can be assigned to a. This means x[0] * y must be computed. First, x is found in the namespace; it computes to addr1. This means that cell 0 at addr1 is found and its value, 3, is returned. Next, y's value, 5, is found. Finally, 3*5 computes to 15.
The next step pauses execution within f and starts execution
at g. A namespace for g is constructed in the local
namespace area,
and the argument, 15, is placed in y's cell there. (This is a
different y than the one in f's namespace!)
Both i.c. and n.s. are reset:
Line 10 constructs a cell in the current namespace (g's):
Line 11 says to compute y*z, which is 90, and return it
to function f. At this point, g's namespace is erased,
and both i.c. and n.s. are reset to their
values just before the call to g:
To finish the assignment at Line 7, 90 is assigned to a's
cell. In the current namespace, a is marked global,
so the cell for a in namespace Test receives 90.
At this point, function f is finished, so its namespace is
erased, and both i.c. and n.s. are reset:
The print command prints 90.
def NAME(PARAMETERS) : DOC_STRING COMMANDswhere
return EXPRESSION
A function call (invocation) has the format,
NAME(ARGUMENTS)
where
A function can also be called with keyword arguments.
A function, F's documentation string can be printed with the command, print F.__doc__
Functions can be written and testing separately, using interactive testing:
$python -i Test.py
>>> fred(..arguments...)This executes the function, just as if it was called from within a program.
There are three important uses of functions: