First, read the second half of Dawson, Chapter 3.
Some jobs must be solved by repeating a step over and over:
Computers became popular partly because a computer program will unfailingly repeat a tedious task over and over. For example, perhaps you want to know the decimal values of the fractions (reciprocals) 1/2, 1/3, 1/4, and so on, up to 1/20. A painful way to calculate these is manually typing the divisions 19 times:
print "1/2 = ", (1.0/2.0) print "1/3 = ", (1.0/3.0) print "1/4 = ", (1.0/4.0) ... print "1/20 = ", (1.0/20.0)A better solution uses a command called a while loop.
At the beginning of the previous section, we saw two ``algorithms'' for tightening a car's wheel and finding a television show. We can rewrite the two algorithms in ``while-loop style,'' where we begin the algorithm with the word, ``while,'' followed by a True-False test, followed by an action to perform as long as the test is True:
A while loop is a ``repeated if-command'', for example:
if lug nut still loose : rotate nut clockwise one turn if lug nut still loose : rotate nut clockwise one turn if lug nut still loose : rotate nut clockwise one turn . . . if lug nut still loose : rotate nut clockwise one turn . . .We keep asking the question and doing the command until we ask and the answer is False.
We write while-loops in Python like this:
while CONDITION : COMMANDswhere CONDITION is a True-False-valued expression and COMMANDs is a sequence of one or more commands, all indented the same amount. The latter is called the loop's body.
With the while-loop, we can write a program that prints the reciprocals from 1/2 to 1/20:
================================== # Reciprocals # prints the reciprocals of 2 to 20. denominator = 2 while denominator <= 20 : print "1/" + str(denominator), "=", (1.0 / denominator) denominator = denominator + 1 ================================Again, think of the while-loop as a ``repeated if'':
if denominator <= 20 : print "1/" + str(denominator), "=", (1.0 / denominator) denominator = denominator + 1 if denominator <= 20 : print "1/" + str(denominator), "=", (1.0 / denominator) denominator = denominator + 1 if denominator <= 20 : print "1/" + str(denominator), "=", (1.0 / denominator) denominator = denominator + 1 . . .
Here is the program's output:
$ python Reciprocals.py 1/2 = 0.5 1/3 = 0.333333333333 1/4 = 0.25 1/5 = 0.2 1/6 = 0.166666666667 1/7 = 0.142857142857 1/8 = 0.125 1/9 = 0.111111111111 1/10 = 0.1 1/11 = 0.0909090909091 1/12 = 0.0833333333333 1/13 = 0.0769230769231 1/14 = 0.0714285714286 1/15 = 0.0666666666667 1/16 = 0.0625 1/17 = 0.0588235294118 1/18 = 0.0555555555556 1/19 = 0.0526315789474 1/20 = 0.05
When this loop executes,
while CONDITION : COMMANDsthe computer does the following:
Here is the flowchart representation of a a while-loop---it is a graph with a cycle that suggests the repetition:
| v +------ > CONDITION ? | / \ | True / \ False | V | | COMMANDS | |_______/ | +-------+ | Vand indeed, this is the origin of the term, ``loop.''
denominator = 2 # Line 1 while denominator <= 20 : # 2 print "1/" + str(denominator), "=", (1.0 / denominator) # 3 denominator = denominator + 1 # 4 # 5When the program starts, the Python interpreter has copied the program into computer storage and has set its instruction counter (``i.c.'') and namespace like this:
i.c.: 1 namespace: (empty)After instruction 1 finishes, we see the new variable, denominator, in the name space, set to 2:
i.c.: 2 namespace: denominator: 2At line 2, the expression denominator <= 20 computes; it is True, so the execution goes to Line 3:
i.c.: 3 namespace: denominator: 2This causes 1.0 / denominator to compute to 0.5, which is printed as 1 / 2 = 0.5.
The instruction counter moves to the next instruction:
i.c.: 4 namespace: denominator: 2Here, denominator is increased by 1 and the instruction counter resets to the start of the loop:
i.c.: 2 namespace: denominator: 3For a second time, denominator <= 20 computes, and it is again True, so the loop's body executes again:
i.c.: 3 namespace: denominator: 3This time, instruction 3 causes 1/3 = 0.333333333333 to print. Next, instruction 4 increments denominator, and we repeat:
i.c.: 2 namespace: denominator: 4
The pattern repeats over and over until we reach this configuration:
i.c.: 2 namespace: denominator: 21At this point, the condition, denominator <= 20, computes to False, and the instruction counter resets to the instruction that follows the loop:
i.c.: 5 namespace: denominator: 21
You should practice execution traces with loops until they become second nature. The reasons are:
You will find it simplest to write an execution trace in a style like that of the previos example. Let's review it: For the reciprocals program just seen,
denominator = 2 # Line 1 while denominator <= 20 : # 2 print "1/" + str(denominator), "=", (1.0 / denominator) # 3 denominator = denominator + 1 # 4 # 5use a pencil and a sheet of paper and draw the instruction counter and name space like this:
i.c. | n.s. --------------------------------------------- 1 |As you encounter each instruction, write its line number in the left column, and draw the variables' cells in the right column, as they appear. When new values are inserted into the variables' cells, show the updates. You might also write notes in your trace about the calculations done at each command. Here is the trace after the first three commands are completed:
i.c. | n.s. --------------------------------------------- 1 | | denominator: 2 2 | (True) | 3 | (printed 1.0/2) |Here is the trace, after Line 4, which changes the value in denominator's cell:
i.c. | n.s. --------------------------------------------- 1 | | denominator: X (overwritten) 2 | (True) | 3 | (printed 1.0/2) | 4 | | denominator: 3 2 |and here is the trace after a second iteration is completed:
i.c. | n.s. --------------------------------------------- 1 | | denominator: X 2 | (True) | 3 | (printed 1.0/2) | 4 | | denominator: X 2 | (True) | 3 | (printed 1.0/3) | 4 | | denominator: 4 2 | . . .Notice how the value for denominator is updated within the loop and notice how the handwritten trace keeps a history of the computation from the beginning to the end. The history can be helpful when we review the loop's progress.
countdown = 10 while countdown != 0 : print i countdown = countdown - 1
countdown = 5 countup = -1 while countdown > countup ) countdown = countdown - 1 print countdown, countup countup = countup + 1
First, recall how an if-command generated its knowledge:
if CONDITION : # assert: CONDITION THEN_COMMANDs # assert: GOAL else : # assert: not CONDITION ELSE_COMMANDs # assert: GOAL # assert, in both cases: GOALUsing the outcome of computing its CONDITION, the if-command chose the appropriate commands for achieving its GOAL.
Loops also have goals to achieve, and these goals must be achieved in stages. Consider yet again the reciprocals program --- its goal is to print all the reciprocals from 2 to 20. The program cannot print these with one command or one if-command; it must repeat a strategy of printing the reciprocals one at a time. While the loop is repeating, it is achieving its goal in stages. Now, we must state precisely what this means.
Recall that a loop is a ``repeated if-command.'' Here is the knowledge production from this structure:
if CONDITION : # assert: CONDITION COMMANDs # assert: PARTIAL_GOAL if CONDITION : # assert: CONDITION and PARTIAL_GOAL COMMANDs # assert: PARTIAL_GOAL if CONDITION : # assert: CONDITION and PARTIAL_GOAL COMMANDs : # assert: PARTIAL_GOAL . . . # assert: all in cases, not CONDITION and PARTIAL_GOALNote that, at each if, the CONDITION is identical and the COMMANDs are identical. This makes the PARTIAL_GOAL identical at all places in the repetition.
The goal of the reciprocals example is to print all the reciprocals from 2 to 20. The body of the program's loop prints one reciprocal. Now, say that the loop has done about half of its work --- what has it accomplished so far ?
We might say, ``the loop has printed the reciprocals from 2 to 11.'' One repetition later, what has the loop accomplished ? We might say, ``the loop has printed the reciprocals from 2 to 12.'' And so on. Notice that the partial goals are stated almost identically --- the only difference appears in the change from 11 to 12, and this difference can be described by the value of variable, denominator.
Here is the program:
denominator = 2 # Line 1 while denominator <= 20 : # 2 print "1/" + str(denominator), "=", (1.0 / denominator) # 3 denominator = denominator + 1 # 4Here is the loop's partial goal, which is maintained at each repetition:
This is worth verifying on the loop's execution trace. We can choose to check the partial goal either at the beginning of each repetition or at the end --- it's the same. We'll do it at both places:
i.c. | n.s. --------------------------------------------- 1 | | denominator: 2 2 | (True) | (all reciprocals from 2 to denominator - 1 have printed) 3 | (printed 1.0/2) | 4 | | denominator: 3 | (all reciprocals from 2 to denominator - 1 have printed) 2 | (True) | (all reciprocals from 2 to denominator - 1 have printed) 3 | (printed 1.0/3) | 4 | | denominator: 4 | (all reciprocals from 2 to denominator - 1 have printed) 2 | . . .
The partial goal holds true while the loop repeats, and when the loop quits, it is because denominator holds the value, 21, implying that all the reciprocals from 2 to 20 have been printed.
This diagram illustrates the form of the argument:
| assert: PARTIAL_GOAL accomplished for 0 repetitions | v False +-------> CONDITION ? -----------------------> assert: PARTIAL_GOAL | | accomplised for all | True | repetitions | V implies: GOAL | assert: PARTIAL_GOAL accomplished | for i repetitions | | | V | COMMANDS | | assert: PARTIAL_GOAL | accomplished | for i+1 repetitions | |_________________+
Based on the graph, we can state the pattern of knowledge acquisition for a while-loop:
# assert: PARTIAL_GOAL while CONDITION : # assert: CONDITION and PARTIAL_GOAL COMMANDs # assert: PARTIAL_GOAL # assert, in all cases: not CONDITION and PARTIAL_GOAL # implies: GOALThe PARTIAL_GOAL is called the loop's invariant property. The purpose of the loop's body is to maintain the PARTIAL_GOAL, which is written in a way that states the loop's successes. When the loop quits, the success at accomplishing and maintaining the PARTIAL_GOAL implies that the loop's ultimate GOAL is achieved. And every loop has an ultimate goal.
Here is the reciprocals program, annotated with its assertions:
denominator = 2 # assert: reciprocals from 2 up to denominator - 1 have printed # (NOTE: at this point, NO reciprocals have been printed. But this is # technically OK --- we have printed the reciprocals from 2 "up to" 1.) while denominator <= 20 : # assert: denominator <= 20 # and reciprocals from 2 up to denominator - 1 have printed print "1/" + str(denominator), "=", (1.0 / denominator) denominator = denominator + 1 # assert: reciprocals from 2 up to denominator - 1 have printed # assert: not(denominator <= 20) # and reciprocals from 2 up to denominator - 1 have printed # implies: reciprocals from 2 up to 20 have printedWhen the loop stops, it is because denominator > 20 (more precisely, it is 21), and this information, plus the partial goal (invariant property) imply that the loop's goal is accomplished: all reciprocals from 2 up to 20 were printed.
If we draw the reciprocals program as a flowchart and use the assertions to label the arcs, we see that the ``knowledge level'' is maintained at all positions within the program while it is active. As we describe in a later section in this chapter, the knowledge levels are like voltage and amperage levels in an electrical circuit.
We will develop the notion of loop invariant later in the chapter.
We often call a definite-iteration loop a ``counting loop,'' because there is often a variable that counts by ones while the loop repeats. (In the reciprocals example seen above, denominator does the counting.)
Here is a second example of definite iteration: Say that we construct a program that computes the average score of a set of student exam scores. The program first asks the student to type the number of exams to average and then it reads the scores, one by one, totals them, and computes the average. The program might behave like this:
$ python AverageScore.py Please type the quantity of exams: 4 Type next score: 76 Type next score: 85 Type next score: 63 Type next score: 88 The average score is: 78
There is a numerical pattern to computing an average score; if the student has taken N exams, then the average score is calculated by this equation:
average = (score_1 + score_2 + ... + score_N) / NThe ellipsis in the formula suggests that we should use a while-loop to sum the scores, one by one, until all N scores are totalled; then we do the division. Here is a flowchart:
read N set total = 0 # remembers the sum of all exam scores | V +----> are there more | scores to read ? --False---> average = total / N | True | | V | read another score | total = total + score | | +--------+The flowchart gives the strategy we will employ --- it is a while loop. The loop must count up to N, so we will need an extra variable to help the loop count while it reads the N scores, one by one. After we read all N scores, we calculate the average as total / N. The flowchart algorithm is easily refined into this Python program:
FIGURE 1: while-loop to compute average score============================= # AverageScore # computes the average of the exam scores submitted by a user # assumed inputs: # N - the number of scores to read; must be a positive int # score_, score_2, ..., score_N - a sequence of exam scores, one per line # guaranteed output: the average of the N exam scores, computed by # average = (score_1 + score_2 + ... + score_N) / N N = int(raw_input("Please type the quantity of exams: ")) count = 0 # the quantity of the scores read so far total = 0 # the points of all the scores read so far while count != N : score = int(raw_input("Type next score: ")) total = total + score count = count + 1 # one more score read! print "\nThe average is", (float(total) / N) # compute a fractional average raw_input("\n\npress Enter to finish") ENDFIGURE================================================================When we test the above program, we see this behavior:
$ python AverageScore.py Please type the quantity of exam: 4 Type next score: 78 Type next score: 86 Type next score: 67 Type next score: 92 The average is 80.75At each iteration of the loop, another score is read and added to the total, and the loop stops after all N scores are read (which is when count == N).
Definite-iteration loops use a loop counter (also called a loop index or sentry variable) that remembers how many iterations are completed. For a loop counter named count, the coding pattern of definite iteration looks like this:
count = INITIAL VALUE while THE CONDITION USING count IS TRUE : EXECUTE COMMANDS INCREMENT countWe saw a counting-upwards instance of the pattern in the previous two examples:
count = START_VALUE while count != STOP_VALUE : COMMANDs count = count + 1
Computer programs are structured somewhat like circuits (cf., the flowcharts we write), and newly built programs should be monitored to verify that the measurements (that is, the values of variables in the namespace) at key points in the program (e.g., at the beginning of a loop body) are acceptable. We can insert print commands to generate execution traces to do this. (If you use an IDE with a debugger, you can ask the IDE to insert breakpoints into the program.) As we saw earlier, execution traces help us understand the loops we write. Rather than write execution traces by hand, we can insert print commands into our programs that make the program generate its own execution trace that we can study carefully. We call this monitoring the program.
We need not insert a print command after every program instruction --- it suffices to insert print commands inside the bodies (indented codes) of the if and while commands and immediately after the commands finish. Each print command should state its position within the program and the values of the variables at that position.
Here is the averaging program of Figure 1, with print commands inserted specifically for generating an execution trace:
====================================== N = int(raw_input("Please type the quantity of exams: ")) count = 0 # the quantity of the scores read so far total = 0 # the points of all the scores read so far while count != N : print "(a) new iteration: N:", N, " count:", count, \ " total:", total score = int(raw_input("Type next score: ")) total = total + score count = count + 1 # one more score read! print "(b) loop finished: N:", N, " count:", count, " total:", total print "\nThe average is", (float(total) / N) raw_input("\n\npress Enter to finish") ============================================The print commands show how the namespace changes each time the loop repeats. Here is the trace generated by one execution:
$ python AverageScore.py Please type the quantity of exams: 4 (a) new iteration: N: 4 count: 0 total: 0 Type next score: 78 (a) new iteration: N: 4 count: 1 total: 78 Type next score: 86 (a) new iteration: N: 4 count: 2 total: 164 Type next score: 67 (a) new iteration: N: 4 count: 3 total: 231 Type next score: 92 (b) loop finished: N: 4 count: 4 total: 323 The average is 80.75The trace lets us confirm that the loop correctly totals the scores as it counts its iterations.
It you wish to ``pause'' the program while it generates its trace, you can do this by inserting raw_input commands as well. For example, we can pause the loop after each iteration by adding this raw_input command after the first trace command:
while count != N : print "(a) new iteration: N:", N, " count:", count, \ " total:", total raw_input("Press Enter to proceed.") # pauses the execution score = int(raw_input("Type next score: ")) total = total + score count = count + 1 # one more score read!
When you design a program with a loop, insert print commands to generate an execution trace, and use the commands while you test your program. When you are satisfied that your program behaves properly, you can insert comment symbols at the front of the print commands to ``turn them off.''
If you have used an IDE (Integrated Development Environment) to write and test your computer programs, then you should read the IDE's user manual to learn how to use its ``breakpoint'' feature to quickly insert print-and-pause execution-trace commands like the ones we have inserted in the above example.
If a programmer wishes to monitor an assertion, she inserts an assert command into the program and tests the program some more. Recall that an assert will halt execution if the stated condition goes false.
In Chapter 2, we saw how to insert assert commands to prevent a disastrous execution step. Here is an example: Division by zero is a serious computational error, and perhaps we write some complex program code that computes a denominator for doing reciprocals. We believe that we have computed the denominator correctly, but at the crucial point in the program, we demand:
... WE COMPUTE A DENOMINATOR ... assert denominator != 0 # for the program to be structurally sound, # denominator must have a non-zero value reciprocal = 1.0 / denominator ...The line marked, assert, states a critical logical property that is required for the computation to proceed correctly. It is a comment that states the programmer is convinced that the prior commands computed a value for the denominator that is non-zero. This is a crucial, structural, invariant property of the program. It is much like a voltage level at a key place in a circuit, calculated from the circuit's schematic by an electronics expert.
Many times an internal specification states a key intermediate property that leads to the program attaining its goal. For example, if a program's goal is to obtain a ship velocity greater than 100, but the physical device that controls the ship's accelerator can at most double velocity at any one command, then we must have this critical internal specification:
assert velocity > 50 velocity = velocity * 2 # goal assertion: velocity > 100The assertion, velocity > 50, must be satisfied by the commands that precede the one that doubles velocity for the program's goal to be obtained.
You can imagine the importance of such internal specifications of programs for X-ray machines, airplane flight controllers, and joysticks for video games --- the programs that read signal input from these devices and direct the devices must maintain invariant properties of the devices' namespaces.
Here is a more realistic example. Consider this little program, which was written with good intentions:
FIGURE==================================================== # RandomReciprocal computes the reciprocal of a randomly chosen int # in the range of 0.5 to 50.5 import random num = (random.randrange(100) + 1) / 2 assert num > 0 # I _believe_ num is always positive ?! print 1.0 / num ===========================================================With an assert command, the programmer has inserted a piece of his thinking, which explains a crucial structural property of the program. This program might be tested hundreds of times with no divsion-by-zero error detected. Nonetheless, the program holds a small flaw --- if random.randrange(100) returns 1 as its answer, then (0+1)/2 computes to 0.
Because the programmer inserted an internal specification, another person might check the program and notice the potential error. Even if no one notices the error, then someday, sometime, the program will be used and the assert statement will detect the violation of the internal specification and announce it so that the programmer is forced to double check his work.
assert commands are a useful way to state your beliefs about the structural properties of the program you have written. They are especially useful during the programming and testing stages, and when a program is put into operation, an assert command can be used to stop a program before it does something that is truly harmful (e.g., deliver a lethal dosage of X-rays to a patient).
Think of an assert command as if it were a voltmeter probe that has been attached to a point in an electrical circuit so that the electrician can monitor the circuit over the first few days/months/years of its use.
Finally, please note that if-commands are the preferred method for checking the quality of external input information that cannot be relied upon:
denominator = int(raw_input("Type an int: ")) # do we trust the human ? if denominator == 0 : denominator = 1 # recover by resetting the value # assert: denominator != 0 reciprocal = 1.0 / denominatorHere, we can make no assertion about the number the human has typed, so we use an if-command to ``filter'' bad values from the rest of the program.
if TEST : print TRACE INFORMATION COMMANDs else : print TRACE INFORMATION COMMANDs print TRACE INFORMATIONFor loops, the ``stress points'' fall at
while TEST : print TRACE INFORMATION COMMANDs print TRACE INFORMATION
assert commands are useful in the same places.
A good electronics engineer uses more than a voltmeter to analyze a circuit --- there is a schematic of the circuit and calculations of the expected voltages and amperages at the key points in the circuit. In computer programming, the ``expected'' measurements must be specified in advance by using ``external'' and ``internal'' specifications.
Just about every item you buy comes with a ``specification'' that describes how the item should be used. For example, a television comes with the specification that it should be used with 110 volts A.C. (otherwise, it will burn up).
Small electronic parts, like resistors and capacitors, come with similar specifications (e.g., a 300 volt D.C. capacitor will function properly up to 300 volts --- then, like the TV, it will burn up and explode). Steel girders also come with specifications that indicate how much weight they can hold. Such specifications are invaluable to the engineer who builds a bridge or building with the girders.
Say that you build a TV set with some resisters and capacitors. If the TV set explodes, it is because a specification of one of the internal parts has been violated (even if the TV itself was used according to its specification of voltage, temperature, and humidity). If a bridge falls down because too many big trucks crossed it simultaneously, it is because a specification of one of the bridge's parts was violated.
The specifications of resistors, capacitors, and girders establish structural invariance properties that must not be violated when the part is used.
Here is a schematic of an amplifier circuit; notice that not only are the parts precisely specified but the levels of voltage and resistance to the wires (pins) connected to the amplifier's vacuum tubes are also stated:
These voltage and resistance levels can be (and should be) measured when the amplifier is assembled and used. If there is a variation from the actual levels from the calculated ones, then amplifier will certainly fail (quickly).
Programs are assemblies like TVs and bridges --- they are assembled from commands. All of the program examples seen in these notes began with comments that described the program's input demands and the output answers that will be computed. These comments specify what the program does. Such program specifications help others use the program correctly; the specifications document the program's behavior. Professional software designers often write the specifications before (or while) they write the program itself.
Our program specifications are written in a careful but informal English and are meant to be read like the specifications that come with the hair dryer or TV you use.
The internals of a program can be complex, and like the internals of a TV or bridge, it is important that the commands within a program are used in ways that maintain critical ``structural invariant properties'' of the program's namespace. A program's internal specifications tend to be technical, written in mathematical or logical notation, stated as True-False (boolean) logical assertions.
This is why we have practicing ``algebra'' for writing and calculating internal assertions of commands. This technique calculates the expected internal behavior of the commands in a program.
Consider the little multiplication program shown in the previous section. Perhaps we test it with 50 test cases, and each time we test it, our execution traces and assert commands and the outputs support our belief that the program computes multiplication. Although our confidence is high, all we really know from our efforts is that for 50 uses of the program, the program behaves properly. What should we do about the infinite number of test cases that we have not tried?
This is not an idle question: when an aircraft company builds a flight-controller program for an airplane, the company dare not install it in a real airplane until the program is ``known'' to be free from errors. In practice, the program is inserted into an airplane simulator and is ``flown'' and monitored (with execution traces and asserts) for some months or years of flight time. Still, there is the possibility that the flight simulator has not tested the program on some bizarre physical experience (e.g., an asteriod hits the cockpit) which might occur in practice. Should the not-completely-trusted program be inserted into real airplanes and allowed to crash and kill people someday?
When programs are used in so-called safety-critical applications (airplanes, medical equipment etc.), the programs should be diagrammed and analyzed as carefully and as completely as any electronics circuit, where the mathematical laws of electronics let a designer calculate in advance the correct levels of amperage and voltages for the points in a circuit. For computer programs, we use algebra to do similar calculations.
A loop is written to accomplish a goal in steps, and each repetition of the loop's body should move one step closer to the goal. When the loop's test finally computes to False, this is a signal that the goal is achieved. A loop like this,
while CONDITION : COMMANDs # assert: GOALmust be dissected and understood in terms of the partial goals it achieves, as if it were a ``repeated if-command'':
if CONDITION : # assert: CONDITION COMMANDs # assert: PARTIAL_GOAL if CONDITION : # assert: CONDITION and PARTIAL_GOAL COMMANDs # assert: PARTIAL_GOAL if CONDITION : # assert: CONDITION and PARTIAL_GOAL COMMANDs : # assert: PARTIAL_GOAL . . . # assert: all in cases, not CONDITION and PARTIAL_GOAL # implies: GOALNote that, at each level of if (loop repetition), the CONDITION is identical and the COMMANDs are identical. This makes the PARTIAL_GOAL identical at all places in the repetition.
We can translate this intuition into the loop structure, like this:
| assert: PARTIAL_GOAL accomplished for 0 repetitions | v False +-------> CONDITION ? -----------------------> assert: PARTIAL_GOAL | | accomplised for all | True | repetitions | V implies: GOAL | assert: PARTIAL_GOAL accomplished | for i repetitions | | | V | COMMANDS | | assert: PARTIAL_GOAL | accomplished | for i+1 repetitions | |_________________+
These informal ideas have a formal, logical depiction. Each repetition maintains a critical internal specification called the loop invariant --- the ``partial goal.'' When the loop finishes, we have that the successful maintenance of the partial goal by all the loop's repetitions tells us that the loop has accomplished its goal:
((loop's CONDITION == False) and (PARTIAL_GOAL holds True)) imply GOAL
In this section, we try to understand the relationship between program monitoring and loop-invariant assertions.
Here is the pattern of knowledge production of a loop, stated in terms of the invariant:
# assert: INVARIANT while CONDITION : # assert: INVARIANT and PARTIAL_GOAL COMMANDs # assert: INVARIANT # assert, in all cases: not CONDITION and INVARIANT # implies: GOAL
When we print an execution trace for a program that contains a counting (definite-iteration) loop, we see that there is a ``pattern'' in the values of the variables updated by the loop. This pattern gives us a big clue about the loop's invariant. Look again at the program that computes an average score:
====================================== N = int(raw_input("Please type the quantity of exams: ")) count = 0 # the quantity of the scores read so far total = 0 # the points of all the scores read so far while count != N : print "(a) new iteration: N:", N, " count:", count, \ " total:", total score = int(raw_input("Type next score: ")) total = total + score count = count + 1 # one more score read! print "(b) loop finished: N:", N, " count:", count, " total:", total print "\nThe average is", (float(total) / N) ============================================and its execution trace:
$ python AverageScore.py Please type the quantity of exams: 4 (a) new iteration: N: 4 count: 0 total: 0 Type next score: 78 (a) new iteration: N: 4 count: 1 total: 78 Type next score: 86 (a) new iteration: N: 4 count: 2 total: 164 Type next score: 67 (a) new iteration: N: 4 count: 3 total: 231 Type next score: 92 (b) loop finished: N: 4 count: 4 total: 323 The average is 80.75There is a pattern that appears at each iteration: total holds the sum of the scores read so far: when count equals 1, total holds the sum of one score; when count equals 2, total holds the sum of two scores, and so on. No matter how often the loop repeats, the pattern remains the same.
We can write the pattern as a mathematical equation: for each trace line labelled by (a) and (b), this mathematical equation holds true:
total == score_1 + score_2 + ... + score_count(Read the underline symbol, _, as a subscript. In this case, score_i denotes the input score that was read at the loop's i-th iteration.
The equation is the loop's invariant property, and here is the averaging program with its invariant property:
FIGURE 2=================================================== N = int(raw_input("Please type the quantity of exams: ")) count = 0 # the quantity of the scores read so far total = 0 # the points of all the scores read so far while count != N : # invariant: total == score_1 + score_2 + ... + score_count score = int(raw_input("Type next score: ")) total = total + score count = count + 1 # one more score read! # assert: not(count != N) and total == score_1 + score_2 + ... + score_count # implies: total == score_1 + score_2 + ... + score_N print "\nThe average is", (float(total) / N) =========================================================ENDFIGUREThe invariant property asserts: At the beginning of each iteration of the loop, no matter how many times the loop body repeats, we know that total == score_1 + score_2 + ... + score_count, where variable count remembers how many scores were read so far.
When the loop terminates, the invariant property is still true, and it lets us conclude that the loop has achieved its goal:
# At the loop's conclusion, we have that count == N. # Therefore, total == score_1 + score_2 + ... + score_NFor this reason, we know that
print "\nThe average score is", (float(total) / N)prints the correct average score.
If we compare the above example to the pattern for loop invariants:
# assert: INVARIANT while CONDITION : # assert: CONDITION and INVARIANT COMMANDs # assert: INVARINT # assert, in all cases: not CONDITION and INVARIANT # implies: GOALwe see that it matches the above example.
When the loop first starts, total is 0 and count is 0, and it is the case that total == score_1 + score_2 + ... + score_count, that is, total holds the total of all zero scores read so far. The invariant is preserved at the beginning and end of each loop repetition, and when the loop finishes, the goal is achieved.
Here is the loop from the program, drawn as a flowchart, which is a kind of circuit schematic for the program. The assertions label the loop's ``wiring'' and show that the loop maintains an invariant ``knowledge level'':
When the loop quits, we know that total holds the sum of all n exam scores, ensuring that the program can calculate the correct average score by dividing by n.
All loops have invariant properties --- after all, a loop's body makes a ``pattern'' in the way it uses variables, updates them, and prints their values. When the loop repeats, its body repeats this pattern. The mathematical of loop invariants lies at the foundation of programming logics, which you will study in a later course.
Here is a second example: What does this program print for z when it finishes? What pattern (invariant property) is computed by its loop?
================================================= x = int(raw_input("type an int: ")) y = int(raw_input("type another: ")) z = 0 count = 0 while count != x : print "(a) x =", x, " y =", y, " z =", z # invariant is ... ? z = z + y count = count + 1 print "(b) x =", x, " y =", y, " z =", z print z =====================================================To better understand, we study the execution trace generated by the print commands:
$ python m.py type an int: 3 type another: 4 (a) x = 3 y = 4 count = 0 z = 0 (a) x = 3 y = 4 count = 1 z = 4 (a) x = 3 y = 4 count = 2 z = 8 (b) x = 3 y = 4 count = 3 z = 12 12The trace information shows, when the loop starts at (a) and after each iteration there is this pattern (invariant):
y * count == zBecause the loop stops exactly when count == x, we conclude that the program halts with z equalling y * x.
The algebra for the loop in the multiplication example is delicate. Here are all the assertions, calculated by algebra, inserted into the program:
================================================ x = int(raw_input("type an int: ")) y = int(raw_input("type another: ")) z = 0 # assert: z = 0 count = 0 # assert: z = 0 and count = 0 # implies: count * y = z while count != x : # assert: count != x and count * y = z z = z + y # assert: count * y = z_old and z_new = z_old + y # implies: (count * y) + y = z_old + y # implies: (count + 1) * y = z_new # implies: (count + 1) * y = z (forget all facts that mention z_old) count = count + 1 # assert: (count_old + 1) * y = z and count_new = count_old + 1 # implies: count_new * y = z # implies: count * y = z (forget all facts that mention count_old) # assert: not(count != x) and count * y = z # implies: x * y = z print z ========================================================These assertions need not be monitored --- they are mathematical properties (like voltage and amperage levels calculated for a circuit schematic), and they will always hold true regardless of how the program is tested and used. For this reason, the program need not be tested an infinite number of times (or even for a few months or a few days) --- the properties are already known to be true. The program has been proved to compute and print the product of its two inputs.
Here is the program's flowchart, labelled with the assertions:
# MaxScore # prints the highest score of the exam scores submitted by a user # assumed inputs: # howmany - the quantity of scores to read; must be nonnegative # exam_1, exam_2, ..., exam_howmany - a sequence of exam scores, one per line # guaranteed output: the largest score in the sequence of exam scores
# DropOneScore # computes the average of the exam scores submitted by a user, but # excludes the one lowest exam score # assumed inputs: # howmany - the quantity of scores to read; must be nonnegative # exam_1, exam_2, ..., exam_howmany - a sequence of exam scores, one per line # guaranteed output: the average of the howmany exam scores, forgetting # the lowest score
t = 4 count = 2 while count <= 4 : t = t * 2 count = count + 1
I like my dog
dog my like I
summation(i) = 0 + 1 + 2 + ... + iFor example, summation(4) is 0 + 1 + 2 + 3 + 4 = 10. So, write a program that computes summation whose header line reads like this:
public int summation(int i)
product(a, b) = a * (a+1) * (a+2) * ... * b(Note: if b > a holds true, then define product(a, b) = 1.) For example, product(3, 6) is 3 * 4 * 5 * 6 = 360.
0! = 1 n! = 1 * 2 * ... * n, for positive nFor example, 5! is 1 * 1 * 2 * 3 * 4 * 5 = 120.
Look again at AverageScore in Figure 1, which sums a sequence of exam scores and computes their average. What happens when the user enters a negative integer as the quantity of exams?
$ python AverageScore.py Please type the quantity of exams: -1 Type next exam score: 78 count = 1 ; total = 78 Type next exam score: 86 count = 2 ; total = 164 Type next exam score: 67 count = 3 ; total = 231 ...Will the program ever stop asking for exam scores? Well, no! The loop iterates indefinitely because its condition is always True. Such behavior is called nontermination, or more crudely, infinite looping or just ``looping.'' A nonterminating loop prevents execution of the commands following the loop, in this case, preventing the program from computing the average.
Although the program's header comment tells us to supply a nonnegative integer as the first input, the program might defend itself with a conditional statement:
N = int(raw_input("Please type the quantity of exams: ")) if N <= 0 : print "Sorry --- the quantity must be nonnegative" else count = 0 # the quantity of the exam scores read so far total = 0 # the points of all the exam scores read so far while count != N : ... # the loop proceeds as beforeSome people perfer to make the loop's entry condition check the input value:
N = int(raw_input("Please type the quantity of exams: ")) count = 0 # the quantity of the exam scores read so far total = 0 # the points of all the exam scores read so far while count < N : ...If N gets a nonnegative value, the loop refuses to iterate even once.
The second alteration is imperfect, because it lets the program compute an erroneous result: If N holds a negative number, then the program computes that the exam average is 0, which is nonsensical, and if N holds zero, the result is even more surprising --- if you try it, you will see this:
$ python AverageScore.py Please type the quantity of exams: 0 The average score is: Traceback (most recent call last): File "AverageScore.py", line 25, in ? print "\nThe average score is:", (total / N) ZeroDivisionError: integer division or modulo by zero
Perhaps looping is unwanted, but under no conditions do we want a program that returns a wrong answer, either. For this reason, you should take care when altering a while-loop's test to ``ensure'' termination; the alteration might cause a wrong answer to be computed and do serious damage.
If your Python program appears to have infinite looping, you can terminate it by pressing the Ctrl (control) and c keys simultaneously.
# printReciprocals # displays the decimal values of the fractions, # 1/2, 1/3, 1/4, ..., one at a time: When started, # it prints: 1/2 = 0.5 # when the user presses Return, it next prints: 1/3 = 0.3333333333 # and when the user presses Return, it prints the next reciprocal, and so on.
Many programs are designed to interact with a user indefinitely --- examples are spreadsheet programs, text editors, and interactive games. Such programs use loops. The loop does not know how many times it must iterate, so it must be prepared to do its job for as long as a user requests.
Here is an example: Say that we write a program that computes reciprocals for whatever integers a person supplies, but quits when the person types a 0. The program might behave like this:
$ python ReciprocalsOnDemand.py Type an int (0 to quit): 3 1 / 3 = 0.333333333333 Type an int (0 to quit): 8 1 / 8 = 0.125 Type an int (0 to quit): 32 1 / 32 = 0.03125 Type an int (0 to quit): 60000 1 / 60000 = 1.66666666667e-05 Type an int (0 to quit): 0 Have a nice day.The program must be designed so that it reads integers one by one, computing their reciprocals, until the user types a zero (and maybe this never happens!).
The algorithm for this kind of indefinite iteration looks like this:
======================= processing = True # remembers if the computation is still going while processing : read num if num == 0 : processing = False # we are finished ! else : print 1.0 / num ============================Here is the complete program:
FIGURE 2: processing input transactions=========================== # ReciprocalsOnDemand # prints the reciprocals for whatever integers the user supplies # assumed inputs: a sequence of nonzero integers, typed one at a time, # finished with a 0 (the signal to quit) # guaranteed output: the reciprocals of the inputs processing = True # remembers if the computation is still going while processing : # invariant: for all user inputs, correct reciprocals were printed num = int(raw_input("Type an int (0 to quit): ")) if num == 0 : processing = False # we are finished ! else : print "1/" + str(num), "=", (1.0 / num) raw_input("\nHave a nice day.") ENDFIGURE=================================================================
An indefinite-iteration loop that does one complete task over and over again (here, printing reciprocals) has as its partial goal (invariant) the correct completion of its task for each input in the sequence of inputs supplied by the user.
The previous example showed us the standard way to process a sequence of input transactions that are submitted one at a time. Here is the pattern:
processing = True while processing : READ AN INPUT TRANSACTION; if THE TRANSACTION INDICATES THAT THE LOOP SHOULD STOP : processing = false else : PROCESS THE TRANSACTIONHere is a second example, a variation of the exam-score-averaging program, where the human does not indicate first how many scores the program should read. Instead, the human merely types the scores, one by one, until she types a negative score, which is the signal for the program to quit:
FIGURE=========================================== # AverageScore # computes the average of the exam scores submitted by a user # assumed inputs: # the exam scores, a sequence of nonnegative ints, terminated by # a negative integer to read; must be a positive int # guaranteed output: the average of the N exam scores, computed by # average = (score_1 + score_2 + ... + score_N) / N count = 0 # the quantity of the scores read so far total = 0 # the points of all the scores read so far processing = True while processing : # invariant: total == score_1 + score_2 + ... + score_count score = int(raw_input("Type next score (-1 to quit): ")) if score < 0 : processing = False else : total = total + score count = count + 1 print "\nThe average is", (float(total) / count) # compute a fractional average raw_input("\n\npress Enter to finish") =====================================================
Here, the iterations of the indefinite-iteration loop are building towards a goal, so the loop invariant is important to the program's correctness.
while True : READ AN INPUT TRANSACTION; if THE TRANSACTION INDICATES THAT THE LOOP SHOULD STOP : break else : PROCESS THE TRANSACTIONThe break command forces an immediate exit from the loop. Here is the reciprocal example recoded with break:
=========================================== while True : num = int(raw_input("Type an int (0 to quit): ")) if num == 0 : break # we are finished --- exit loop immediately else : print "1/" + str(num), "=", (1.0 / num) raw_input("\nHave a nice day.") ===============================================
s = "" ... s = s + a_line_of_text + "\n"
Following is a case study that shows how we can do this in Python.
The program might behave like this, when we make an initial deposit of $200.000, then write a check of $52.69, then ask to view the ledger, and then quit:
$ python Checkbook.py Type request: d(eposit), c(heck), v(iew), q(uit): d Type amount: $200.00 Type date: April 1 Type request: d(eposit), c(heck), v(iew), q(uit): c Type amount: $52.69 Type date and to whom payable: April 3 BookStore Co. Type request: d(eposit), c(heck), v(iew), q(uit): v April 1 deposit 200.00 April 3 BookStore Co. check 52.69 Current balance is: 147.31 Type request: d(eposit), c(heck), v(iew), q(uit): q Have a nice day.The program must keep a history, (``ledger'') of all the transactions so that it can print them when the user requests (by typing v). There are two important variables that the program must maintain:
To make the program interact with its user in the behavior shown above, we can use the input-processing pattern seen in the previous section as the program's ``skeleton'' (algorithm). Because we have four forms of input command (deposit, write a check, the main part of the input-processing pattern is an if-elif-command. The algorithm looks like this:
balance = 0 # the account's balance starts at 0 cents ledger = "" # the history of transactions starts empty processing = True while processing : read the transaction if transaction == "q" : processing = False elif transaction == "c" : process a check-writing request (to be refined further!) elif the transaction == "d" process a deposit (to be refined further, also) elif the transation == "v" : print the contents of the ledger and the balance (ditto) else : the transaction is bad, so print an error message (again)The algorithm shows there are several tasks that the program must tell how to do.
It is too much work to write all the tasks at once and then code all of them and then test all of them; our thinking will get distracted and our testing will probably miss crucial test cases. It is better to refine and code and test each task one at a time.
Let's start with the the deposit transition ("d"). To process a deposit, we think about how we would write down a deposit (if we were using pencil and paper) and would update the account balance. We decide on these steps:
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 ledgerNow we write the code in Python:
input = raw_input("Type amount: $") dollars_and_cents = input.split(".") # This trick is explained below dollars = int(dollars_and_cents[0]) # same here cents = int(dollars_and_cents[1]) # same here amount = (dollars * 100) + cents # compute the deposit in cents date = raw_input("Type date: ") balance = balance + amount # add the deposit to the balance info = date + " deposit " + input ledger = ledger + info + "\n" # add the info to the ledgerLines 2-4 above use a trick from the previous chapter:
dollars_and_cents = input.split(".")splits a dollars-period-cents string into its dollars and cents amounts, which are then assigned to their respective variables:
dollars = int(dollars_and_cents[0]) cents = int(dollars_and_cents[1])(The details of the trick are explained in Chapter 5.)
The last line shows that we add a transaction to the end of the ledger by string concatenation.
Here's the program we have written so far in Python:
=================================== # Checkbook # maintains a ledger of checkbook transactions and a running balance ledger = "" balance = 0 # the account's balance starts at 0 cents 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 pass # process a check-writing request elif request == "d" : # Deposit input = raw_input("Type amount: $") dollars_and_cents = input.split(".") dollars = int(dollars_and_cents[0]) cents = int(dollars_and_cents[1]) amount = (dollars * 100) + cents note = raw_input("Type date: ") balance = balance + amount ledger = ledger + note + " deposit " + input + "\n" print ledger, balance # generate trace information for testing elif request == "v" : # View all transactions pass # print the contents of the ledger and the balance else : print "I don't understand your request; please try again." raw_input("\nHave a nice day.") ========================================We inserted the coding for doing deposits. Notice the cute trick: the unfinished parts of the algorithm are represented by pass, which is a Python command that stands for ``do nothing.''
Although the program is not finished, we can already test what we have! We can start the program, try it with deposit requests, and quit when we want. Once we believe that the program handles deposits correctly, we can code more of the program.
Note that we added the extra command,
print ledger, balance # generate trace information for testingso that we can see what the program does when it executes the deposit action.
Say that we next refine and code the command to view the ledger. That coding is simple:
print ledger print "Current balance is:" , dollars = balance / 100 cents = balance % 100 print dollars, ".", centsWe insert these instructions into the position held by
pass # print the contents of the ledger and the balanceand test the program with both ``deposit'' ("d") and ``view'' ("v") requests. (If you test the above coding, you will find that it does not print correctly amounts like 500.05 --- it prints 500.5 instead. This must be repaired, later.)
Once we refine and code the check request ("c"), which is similar to the deposit part, we get this program:
FIGURE 3 =========================================== # Checkbook # maintains a ledger of checkbook transactions and a running balance # # assumed input: a series of transactions of four forms: # (i) Deposits: type d # then type the dollars and cents amount # then type the date of the deposit # (ii) Check written: type c # then type the dollars and cents amount # then type the date and to whom the check was addressed # (iii) View ledger: type v # (iv) Quit: type q # # guaranted output: a correct ledger, listing the sequence of deposits # and checks, along with the final balance, generated by the v command ledger = "" # the history of all transactions balance = 0 # maintain the balance in cents processing = True while processing : # invariant: ledger holds a history of all transactions so far # and balance maintains the correct balance from the transactions request = raw_input("\nType request: d(eposit), c(heck), v(iew), q(uit): ") if request == "q" : # Quit processing = False elif request == "c" : # Check written input = raw_input("Type amount: $") dollars_and_cents = input.split(".") dollars = int(dollars_and_cents[0]) cents = int(dollars_and_cents[1]) amount = (dollars * 100) + cents # compute the amount in cents note = raw_input("Type date and to whom payable: ") balance = balance - amount # deduct the check from the balance info = note + " check " + input ledger = ledger + info + "\n" elif request == "d" : # Deposit input = raw_input("Type amount: $") dollars_and_cents = input.split(".") dollars = int(dollars_and_cents[0]) cents = int(dollars_and_cents[1]) amount = (dollars * 100) + cents # compute the deposit in cents date = raw_input("Type date: ") balance = balance + amount # add the deposit to the balance info = date + " deposit " + input ledger = ledger + info + "\n" elif request == "v" : # View all transactions print ledger print "Current balance is:" , dollars = balance / 100 cents = balance % 100 if cents < 10 : print str(dollars) + ".0" + str(cents) else : print str(dollars) + "." + str(cents) else : print "I don't understand your request; please try again." raw_input("\nHave a nice day.") ENDFIGURE====================================================Notice that one way of printing a correct dollars, cents amount for, say, 500 dollars and 5 cents is by adding an if-command:
dollars = balance / 100 cents = balance % 100 if cents < 10 : print str(dollars) + ".0" + str(cents) else : print str(dollars) + "." + str(cents)There is a more clever way to insert the optional zero, which uses a Python trick. The trick cannot be explained just now, but it looks like this:
dollars = balance / 100 cents = balance % 100 import string # the string module has a ``magic'' operation, zfill print str(dollars) + "." + string.zfill(cents, 2) # format cents so that # it always prints as 2 digitsFinally, we might ask, ``What is the invariant property of the loop in this program?'' The answer is: the loop correctly maintains the value of balance so that it correctly added all deposits and subtracted all checks, and it also correctly maintains the value of ledger so that it remembers all transactions. Perhaps these invariants are not so simply written as mathematical equations, but they are critical to understanding what the program does!
Loops are easily miswritten, and one big problem is that a loop's termination condition is often carelessly written. For example, this attempt to compute the sum, 1 + 2 + ... + n,
total = 0 i = 0 while i <= n : i = i + 1 total = total + i print "i = ", i, "; total = ", total # generates a tracefails because the loop iterates one time too many. This form of error, where a loop iterates one time too many or one time too few, is commonly made, so be alert for it.
A related problem is an improper starting value for the variables used by the loop, e.g.,
total = 1 i = 1 while i <= n : total = total + i i = i + 1 print "i = ", i, "; total = ", total # generates a traceThis loop again attempts to compute the sum, 1 + 2 + ... + n, but it adds the value 1 twice into its total.
Examples like these show that it is not always obvious when a loop iterates exactly the correct number of times. When you test a loop with example data, be certain to know the correct answer so that you can compare it to the loop's output.
Here is another technical problem with loop tests: Never code a loop's test expression as an equality of two floats (fractional numbers). For example, this loop, which counts in thirteenths from zero to one,
d = 0.0 while d != 1.0 : d = d + (1.0 / 13.0) print dshould terminate in 13 iterations but loops forever due to imprecise fractional computer arithmetic.
Testing programs with loops is more difficult than testing programs with conditionals, because it is not enough to merely test each statement in the loop body once. A loop encodes a potentially infinite number of distinct executions, implying that an infinite number of test cases might be needed. Obviously, no testing strategy can be this exhaustive.
In practice, one narrows loop testing to these test cases:
Consider yet another attempt to compute the summation 1 + 2 + ... + n:
n = int(raw_input("Type an int: ")) total = 0 i = 0 while i != n : total = total + i i = i + 1 print "i = ", i, "; total = ", total # generates a traceThis program might be tested with n set to 0 (which we believe should cause immediate termination), set to 1 (should cause termination in one iteration), set to, say, 4 (a typical case that requires multiple iterations), and set to a negative number, say, -1, (which might cause unwanted behavior). These test cases quickly expose that the loop's condition forces termination one iteration too soon. Subtle errors arise when a loop's condition stops the loop one iteration too soon or one iteration too late; testing should try to expose these ``boundary cases.''
A more powerful version of loop testing is invariant monitoring, where you insert an assert command into the loop to monitor how the loop maintains the variables in the namespace. If the invariant is remaining true, this gives you great confidence that the loop is making proper progress towards the correct answer.
Although loop testing can never be exhaustive, please remember that any testing is preferable to none---the confidence that one has in one's program increases by the number of test cases that the program has successfully processed.
while CONDITION : COMMANDsThe semantics goes as follows:
assert BOOLEAN_EXPRESSIONwhere BOOLEAN_EXPRESSION is an expression that computes to True or False. The semantics goes
count = INITIAL VALUE while CONDITION ON count : EXECUTE COMMANDs INCREMENT countwhere count is the sentry variable.
processing = True # announces when it's time to stop while processing : READ AN INPUT TRANSACTION if THE INPUT INDICATES THAT THE LOOP SHOULD STOP : processing = False else : PROCESS THE TRANSACTION
There is a variation on the above pattern that uses the break command:
while True : READ AN INPUT TRANSACTION; if THE TRANSACTION INDICATES THAT THE LOOP SHOULD STOP : break else : PROCESS THE TRANSACTIONThe break causes the loop to terminate immediately, without executing any more commands in its body.