3.3. While Statements¶
3.3.1. Simple while
Loops¶
Other than the trick with using a return
statement inside of a
for
loop, all of the loops so far have gone all the way through
a specified list. In any case the for
loop has required the
use of a specific list. This is often too restrictive. A Python
while
loop behaves quite similarly to common English usage. If
I say
While your tea is too hot, add a chip of ice.
Presumably you would test your tea. If it were too hot, you would add a little ice. If you test again and it is still too hot, you would add ice again. As long as you tested and found it was true that your tea was too hot, you would go back and add more ice. Python has a similar syntax:
while
condition:
- indentedBlock
Setting up the English example in a similar format would be:
- while your tea is too hot :
- add a chip of ice
To make things concrete and numerical, suppose the following: The tea starts at 115 degrees Fahrenheit. You want it at 112 degrees. A chip of ice turns out to lower the temperature one degree each time. You test the temperature each time, and also print out the temperature before reducing the temperature. In Python you could write and run the code below, saved in example program cool.py:
1 2 3 4 5 6 | temperature = 115
while temperature > 112: # first while loop code
print(temperature)
temperature = temperature - 1
print('The tea is cool enough.')
|
I added a final line after the while loop to remind you that execution follows sequentially after a loop completes.
If you play computer and follow the path of execution, you could
generate the following table. Remember, that each time you reach
the end of the indented block after the while
heading,
execution returns to the while
heading for another test:
Line | temperature | Comment |
---|---|---|
1 | 115 | |
2 | 115 > 112 is true, do loop | |
3 | prints 115 | |
4 | 114 | 115 - 1 is 114, loop back |
2 | 114 > 112 is true, do loop | |
3 | prints 114 | |
4 | 113 | 114 - 1 is 113, loop back |
2 | 113 > 112 is true, do loop | |
3 | prints 113 | |
4 | 112 | 113 - 1 is 112, loop back |
2 | 112 > 112 is false, skip loop | |
6 | prints that the tea is cool |
Each time the end of the indented loop body is reached, execution
returns to the while
loop heading for another test. When the
test is finally false, execution jumps past the indented body of
the while
loop to the next sequential statement.
A while
loop generally follows the pattern of the successive
modification loop introduced with for
-each loops:
initializationwhile
continuationCondition:
do main action to be repeatedprepare variables for the next time through the loop
Test yourself: Following the code. Figure out what is printed. :
i = 4
while i < 9:
print(i)
i = i+2
Check yourself by running the example program testWhile.py
.
Note
In Python, while
is not used quite like in English. In
English you could mean to stop as soon as the condition you want
to test becomes false. In Python the test is only made when
execution for the loop starts (or starts again),
not in the middle of the loop.
Predict what will happen with this slight variation on the previous example, switching the order in the loop body. Follow it carefully, one step at a time.
1 2 3 4 | i = 4 # variation on testWhile.py
while (i < 9):
i = i+2
print(i)
|
Check yourself by running the example program testWhile2.py
.
The sequence order is important. The variable i
is increased before
it is printed, so the first number printed is 6. Another common
error is to assume that 10 will not be printed, since 10 is
past 9, but the test that may stop the loop is not made in the
middle of the loop. Once the body of the loop is started, it
continues to the end, even when i
becomes 10.
Line | i | Comment |
---|---|---|
1 | 4 | |
2 | 4 < 9 is true, do loop | |
3 | 6 | 4+2=6 |
4 | print 6 | |
2 | 6 < 9 is true, do loop | |
3 | 8 | 6+2= 8 |
4 | print 8 | |
2 | 8 < 9 is true, do loop | |
3 | 10 | 8+2=10 No test here |
4 | print 10 | |
2 | 10 < 9 is false, skip loop |
Predict what happens in this related little program:
nums = list()
i = 4
while (i < 9):
nums.append(i)
i = i+2
print(nums)
Check yourself by running the example program testWhile3.py
.
3.3.2. The Most General range
Function¶
There is actually a much simpler way to generate the previous
sequences like in testWhile3.py
,
using a further variation of the range
function.
Enter these lines separately in the Shell. As in the simpler
applications of range
, the values are only generated one at a
time, as needed. To see the entire sequence at once, convert the
sequence to a list before printing:
nums = range(4, 9, 2)
print(list(nums))
The third parameter for the range function is the step size. It is needed when the step size from one element to the next is not 1.
The most general syntax is
range(
start,
pastEnd,
step)
The value of the second parameter is always past the final element of the list. Each element after the first in the list is step more than the previous one. Predict and try in the Shell:
list(range(4, 10, 2))
Actually the range function is even more sophisticated than
indicated by the while
loop above. The step size can be negative.
Try in the Shell:
list(range(10, 0, -1))
Do you see how 0 is past the end of the list?
Try it: Make up a range
function call to generate the list of
temperatures printed in the tea example, 115, 114, 113
. Test it
in the Shell.
These ranges, like the simpler ranges that we used earlier,
are most often used as the sequence in a for
loop heading:
for i in range(10, 0, -1): # countdown...
print(i)
print('Blastoff!')
3.3.3. Interactive while
Loops¶
The earlier examples of while loops were chosen for their
simplicity. Obviously they could have been rewritten with range
function calls. Now lets try a more interesting example. Suppose
you want to let a user enter a sequence of lines of text, and want
to remember each line in a list. This could easily be done with a
simple repeat loop if you knew the number of lines to enter. For
example, in readLines0.py
, the user is prompted for the exact
number of lines to be entered:
lines = list()
n = int(input('How many lines do you want to enter? '))
for i in range(n):
line = input('Next line: ')
lines.append(line)
print('Your lines were:') # check now
for line in lines:
print(line)
The user may want to enter a bunch of lines and not count them all
ahead of time. This means the number of repetitions would not be
known ahead of time. A while
loop is appropriate here. There is
still the question of how to test whether the user wants to
continue. An obvious but verbose way to do this is to ask before
every line if the user wants to continue, as shown below and in the
example file readLines1.py
. Read it and then run it:
lines = list()
testAnswer = input('Press y if you want to enter more lines: ')
while testAnswer == 'y':
line = input('Next line: ')
lines.append(line)
testAnswer = input('Press y if you want to enter more lines: ')
print('Your lines were:')
for line in lines:
print(line)
See the two statements setting testAnswer
:
one before the while
loop and one at the bottom of the loop body.
Note
The data must be initialized before the loop, in order for the first test of the while condition to work. Also the test must work when you loop back from the end of the loop body. This means the data for the test must also be set up a second time, in the loop body (commonly as the action in the last line of the loop). It is easy to forget the second time!
The readLines1.py
code works, but it may be more annoying than
counting ahead! Two lines must be entered for every one you
actually want! A practical alternative is to use a sentinel: a
piece of data that would not make sense in the regular sequence,
and which is used to indicate the end of the input. You could
agree to use the line DONE!
Even simpler: if you assume all the
real lines of data will actually have some text on them, use an
empty line as a sentinel. (If you think about it, the Python
Shell uses this approach when you enter a statement with an
indented body.) This way you only need to enter one extra (very
simple) line, no matter how many lines of real data you have.
What should the while condition be now? Since the sentinel is an
empty line, you might think line == ''
, but that is the
termination condition, not the continuation condition: You need
the opposite condition. To negate a condition in Python, you may
use not
, like in English,
not line == ''
Of course in this situation there is a shorter way,
line != ''
Run the example program readLines2.py
, shown below:
lines = list()
print('Enter lines of text.')
print('Enter an empty line to quit.')
line = input('Next line: ') # initalize before the loop
while line != '': # while NOT the termination condition
lines.append(line)
line = input('Next line: ') # !! reset value at end of loop!
print('Your lines were:')
for line in lines:
print(line)
Again the data for the test in the while loop heading must be
initialized before the first time the while
statement is
executed and the test data must also be made ready inside the
loop for the test after the body has executed. Hence you see the
statements setting the variable line
both before the loop and
at the end of the loop body. It is easy to forget the second place
inside the loop!
After reading the rest of this paragraph,
comment the last line of the loop out, and run it again:
It will never stop! The
variable line
will forever have the initial value you gave it!
You actually can stop the program by entering Ctrl-C
. That means
hold the Ctrl
key and press c
.
Note
As you finish coding a while
loop, it is good practice to
always double-check: Did I make a change to the variables, inside
the loop, that will eventually make the loop condition False
?
The earliest while
loop examples had numerical tests and the code
to get ready for the next loop just incremented a numerical variable
by a fixed amount. Those were simple examples but while
loops
are much more general! In the interactive loop we have seen a continuation
condition with a string test, and getting ready for the next time through
the loop involves input from the user.
Some of the exercises that follow involve interactive while loops. Others were delayed until here just because they have a wider variety of continuation condition tests and ways to prepare for the next time through the loop. What is consistent is the general steps to think of and questions to ask yourself. They keep on applying! Keep these in mind!
- the need to see whether there is a kind of repetition, even without a fixed collection of values to work through
- to think from the specific situation and figure out the continuation condition that makes sense for your loop
- to think what specific processing or results you want each time through the loop, using the same code
- to figure out what supporting code you need to make you ready for the next time through the loop: how to make the same results code have new data values to process each time through, and eventually reach a stopping point.
Detecting the need for while
statements:
Like with planning programs needing``for`` or if
statements, you want to be able to translate English descriptions of problems that would naturally include while
statements. What are some words or phrases or ideas that suggest the use of
these statements? Think of your own and then compare to a few I gave: [1]
3.3.3.1. Interactive Sum Exercise¶
Write a program sumAll.py
that prompts the user to enter
numbers, one per line, ending with a line containing only 0, and keep a
running sum of the numbers. Only print out the sum after all the
numbers are entered (at least in your final version).
Do not create a list! Each time you read in a number, you can
immediately use it for your sum, and then be done with the number
just entered.
3.3.3.2. Safe Number Input Exercise¶
* There is an issue with reading in numbers with the input statement. If you make a typo and enter something that cannot be converted from a string to the right kind of number, a naive program will bomb. This is avoidable if you test the string and repeat if the string is illegal. In this exercise write safe utility function replacements for the input function that work to read in a whole number, an integer or a decimal number.
All parts refer to the previous
Is Number String Exercise. Part a. refers to the introduction in the
previous exercise.
Parts b. and c. refer to functions in the solution, isNumberStr.py
, of
the previous exercise.
Make sure you look back at these first.
Save the example safeNumberInputStub.py
as safeNumberInput.py
,
and complete it.
It contains headings and documentation strings
for the functions in each part of this exercise.
- This part considers the simplest case,
where you are trying to enter a whole number.
Complete the definition of the function
safeWholeNumber
. - Complete the function
safeInt
. This easily parallels part a. if you copy in and use the function (not method)isIntegerStr
. - Complete the function
safeDecimal
. This easily parallels part b. if you copy in and use the functionisDecimalStr
.
3.3.3.3. Savings Exercise¶
The idea here is to see how many years it will take a bank account to grow
to at least a given value, assuming a fixed annual interest.
Write a program savings.py
.
Prompt the user for three numbers: an initial balance, the annual percentage
for interest as a decimal, like .04 for 4%, and the final balance desired.
All the monetary amounts that you print should be rounded to exactly two decimal places. Start by printing the initial balance this way. For example, if the initial balance was entered as 123.5, it should be reprinted by your program as 123.50. Also print the balance each year until the desired amount is reached or passed. The first balance at or past the target should be the last one printed.
The math: The amount next year is the amount now times (1 + interest fraction), so if I have $500 now and the interest rate is .04, I have $500*(1.04) = $520 after one year and after two years I have, $520*(1.04) = $540.80....
For example, if I respond to the prompts, and enter into the program a $500 starting balance, .04 interest rate and a target of $550, the program prints:
500.00
520.00
540.80
562.43
3.3.3.4. Strange Sequence Exercise¶
* Recall Strange Function Exercise and its jumpFunc.py
which contains the function jump
:
For any integer n, jump(n) is n//2 if n is even, and 3*n+1 if n is odd.
You can start with one number, say n = 3, and keep applying the jump function to the last number given, and see how the numbers jump around!
jump(3) = 3*3+1 = 10; jump(10) = 10//2 = 5;
jump(5) = 3*5+1 = 16; jump(16) = 16//2 = 8;
jump(8) = 8//2 = 4; jump(4) = 4//2 = 2;
jump(2) = 2//2 = 1
This process of repeatedly applying the same function to the most recent result is called function iteration. In this case you see that iterating the jump function, starting from n=3, eventually reaches the value 1.
It is an open research question whether iterating the jump function from an integer n will eventually reach 1, for every starting integer n greater than 1. Researchers have only found examples of n where it is true. Still, no general argument has been made to apply to the infinite number of possible starting integers.
In this exercise you iterate the jump function for specific starting values n, until the result is 1.
Save example
jumpSeqStub.py
asjumpSeq.py
and complete the missing function bodies. If you coded the functionjump
before injumpFunc.py
, you can copy it. You can complete eitherprintJumps
orlistJumps
first, and test before completing the other. Hint [2]After you have finished and saved
jumpSeq.py
copy it and save the file asjumpSeqLengths.py
.First modify the main method so it prompts the user for a value of n, and then prints just the length of the iterative sequence from listJumps(n). Hint [3]
Then elaborate the program so it prompts the user for two integers: a lowest starting value of n and a highest starting value of n. For all integers n in the range from the lowest start through the highest start, including the highest, print a sentence giving the starting value of n and the length of the list from
listJumps(n)
. An example run:Enter lowest start: 3Enter highest start: 6Starting from 3, jump sequence length 8.Starting from 4, jump sequence length 3.Starting from 5, jump sequence length 6.Starting from 6, jump sequence length 9.
3.3.4. Graphical Applications¶
Another place where a while
loop could be useful is in
interactive graphics. Suppose you want the user to be able to
create a Polygon by clicking on vertices they choose interactively,
but you do not want them to have to count the number of vertices
ahead of time. A while
loop is suggested for such a repetitive
process. As with entering lines of text interactively, there is the
question of how to indicate that you are done (or how to indicate to
continue). If you make only a certain region be allowed for the
Polygon, then the sentinel can be a mouse click outside the region.
The earlier interactive color choice example already has a method
to check if a mouse click is inside a Rectangle, so that method can
be copied and reused.
Creating a polygon is a unified activity with a clear result, so let’s define a function. It involves a boundary rectangle and mouse clicks in a GraphWin, and may as well return the Polygon constructed. Read the following start:
def polyHere(rect, win):
''' Draw a polygon interactively in Rectangle rect, in GraphWin win.
Collect mouse clicks inside rect into the vertices of a Polygon,
and always draw the Polygon created so far.
When a click goes outside rect, stop and return the final polygon.
The Polygon ends up drawn. The method draws and undraws rect.
'''
It is useful to start by thinking of the objects needed, and give them names.
- A Polygon is needed. Call it
poly
. - A list of vertices is needed. Call it
vertices
. I need to append to this list. It must be initialized first. - The latest mouse click point is needed. Call it
pt
.
Certainly the overall process will be repetitious, choosing point after point. Still it may not be at all clear how to make an effective Python loop. In challenging situations like this it is often useful to imagine a concrete situation with a limited number of steps, so each step can be written in sequence without worrying about a loop.
For instance to get up to a triangle (3 vertices in our list and a fourth mouse click for the sentinel), you might imagine the following sequence, undrawing each old polygon before the next is displayed with the latest mouse click included:
rect.setOutline('red')
rect.draw(win)
vertices = list()
pt = win.getMouse()
vertices.append(pt)
poly = Polygon(vertices)
poly.draw(win) # with one point
pt = win.getMouse()
poly.undraw() # missing latest point
vertices.append(pt)
poly = Polygon(vertices)
poly.draw(win) # with two points
pt = win.getMouse()
poly.undraw() # missing latest point
vertices.append(pt)
poly = Polygon(vertices)
poly.draw(win) # with three points
pt = win.getMouse() # assume outside the region
rect.undraw()
return poly
There is a fine point here that I missed the first time. The
vertices of an existing Polygon do not get mutated in this system. A new
Polygon gets created each time with the new vertex list. The old
Polygon does not go away automatically, and extraneous lines appear
in the picture if the old polygon is not explicitly undrawn each
time before a new version is redrawn with an extra vertex.
The last Polygon you draw should be visible at the end, so in the example
above where I was assuming the third click was the last for the triangle,
I did not undraw
the Polygon.
The timing for each undraw
needs to be after the next mouse click
and presumably before the revised Polygon is created, so it could be
before or after the line vertices.append(pt)
. I arbitrarily
chose for it to go before the vertices list is changed. The rest
of the order of the lines is pretty well fixed by the basic logic.
If you think of the repetitions through a large number of loops,
the process is essentially circular (as suggested by the word
‘loop’). The body of a loop in Python, however, is written as a
linear sequence: one with a first line and a last line, a
beginning and an end. We can cut a circular loop anywhere to get a piece
with a beginning and an end. In practice, the place you cut the
loop for Python has one main constraint: The processing in
Python from the end of one time through the loop to the beginning
of the next loop is
separated by the test of the condition in the heading.
The continuation condition
in the while
heading must make sense where you cut the loop.
It can help to look at a concrete example sequence, like the steps
listed above for creating a triangle, only now assuming we do not know
how many vertices will be chosen. The continuation condition is
for pt
to be in the rectangle, so using the previously written
function isInside
, the loop heading will be
while isInside(pt, rect):
With this condition in mind, look for where to split to loop. It
needs to be after a new pt
is clicked (so it can be tested) and
before the next Polygon is created (so it does not include the
sentinel point by mistake). In particular, with the sequence above,
look and see that the split could go before or after the
poly.undraw()
line. Exercise Moving Undraw considers
the case where the split goes before this line. I will proceed with
the choice of splitting into a Python loop after the undraw
line.
This makes the loop be
while isInside(pt, rect):
vertices.append(pt)
poly = Polygon(vertices)
poly.draw(win)
pt = win.getMouse()
poly.undraw()
If you follow the total sequence of required steps above for making
the concrete triangle, you see that this full sequence for the
loop is only repeated twice. The last time there is no
poly.undraw()
step. I could redo the loop moving the undraw
line to the top, which caused different issues
(Exercise Moving Undraw below). Instead think how to make it work at the
end of the final time through the loop....
There are several possible approaches. You want the undraw
line
every time except for the last time. Hence it is a statement you
want sometimes and not others. That suggests an if
statement.
The times you want the undraw
are when the loop will repeat
again. This is the same as the continuation condition for the loop,
and you have just read the next value for pt
! You could just
add a condition in front of the last line of the loop:
if isInside(pt, rect):
poly.undraw()
I find this option unaesthetic: it means duplicating the continuation test twice in every loop.
Instead of avoiding the undraw
as you exit the loop, another
option in this case is to undo it: just redraw the polygon one
final time beyond the loop. This only needs to be done once, not
repeatedly in the loop. Then the repetitious lines collapse neatly
into the loop.
If you look at the overall concrete sequence for the triangle, not all the lines are in the loop. You must carefully include the lines both that come before the loop and those that come after the loop. Make sure these lines are not put in the loop, but before or after, as indicated by the concrete sequence in the example. In the end the entire function is:
def polyHere(rect, win):
''' Draw a polygon interactively in Rectangle rect, in GraphWin win.
Collect mouse clicks inside rect into the vertices of a Polygon,
and always draw the Polygon created so far.
When a click goes outside rect, stop and return the final polygon.
The Polygon ends up drawn. The method draws and undraws rect.
'''
rect.setOutline("red")
rect.draw(win)
vertices = list()
pt = win.getMouse()
while isInside(pt, rect):
vertices.append(pt)
poly = Polygon(vertices)
poly.draw(win)
pt = win.getMouse()
poly.undraw()
poly.draw(win)
rect.undraw()
return poly
Make sure you understand:
Follow this code through, imagining three mouse clicks inside rect
and then one click outside of rect. Compare the steps to the ones
in the concrete sequence written out above and see that the match
(aside from the last canceling undraw
and draw
of poly
).
This function is illustrated in the example program
makePoly.py
. Other than standard graphics example code, the
main program contains:
rect1 = Rectangle(Point(5, 55), Point(200, 120))
poly1 = polyHere(rect1, win)
poly1.setFill('green')
rect2 = Rectangle(Point(210, 50), Point(350, 350))
poly2 = polyHere(rect2, win)
poly2.setOutline('orange')
As you can see, the returned polygons are used to make color changes, just as an illustration.
In earlier animation examples a while
loop would also have been
useful. Rather than continuing the animation a fixed number of
times, it would be nice for the user to indicate by a mouse click
when she has watched long enough. Thus far the only way to use the
mouse has been with getMouse()
. This is not going to work in an
animation, because the computer stops and waits for a click with
getMouse()
, whereas the animation should continue until the
click.
In full-fledged graphical systems that respond to events, this is
no problem. Zelle’s graphics is built on top of a capable
event-driven system, and in fact, all mouse clicks are registered,
even outside calls to getMouse()
.
As an example, run example program randomCirclesWhile.py
. Be
sure to follow the prompt saying to click to start and to end.
Aside from the prompts, the difference from the previous
randomCircles.py
program is the replacement of the original
simple repeat loop heading
for i in range(75):
by the following initialization and while loop heading:
while win.checkMouse() == None: #NEW*
The graphics module remembers the last mouse click, whether or not
it occurred during a call to getMouse()
. A way to
check if the mouse has been clicked since the last call to getMouse()
is checkMouse()
. It does not wait for the mouse as in
getMouse()
. Instead it returns the remembered mouse click -
the most recent mouse click in the past, unless there has been no
mouse click since the last call to getMouse or checkMouse. In that
case checkMouse()
returns None (the special object used to
indicate the lack of a regular object).
The checkMouse
method allows for a loop that does not stop
while waiting for a mouse click, but goes on until the heading test
detects that the mouse was clicked.
A similar elaboration can be made for the other examples of
animation, like bounce1.py
. In bounceWhile.py
I modified
bounce1.py
to have a while loop in place of the for-loop
repeating 600 times. Run it. The only slight added modification
here was that win
was not originally a parameter to
bounceInBox
, so I included it. Look at the source code for
bounceWhile.py
, with the few changes marked NEW.
In bounce2.py
I also made a more interesting change to the
initialization, so the initial direction and speed of the mouse are
determined graphically by the user, with a mouse click. Try example
program bounce2.py
.
The program includes a new utility function to help determine the initial (dx, dy) for the animation. This is done by calculating the move necessary to go from one point (where the ball is in this program) to another (specified by a user’s mouse click in this program). :
def getShift(point1, point2): # NEW utility function
'''Returns a tuple (dx, dy) which is the shift from point1 to point2.'''
dx = point2.getX() - point1.getX()
dy = point2.getY() - point1.getY()
return (dx, dy)
Since the function calculates both a change in x and y, it returns
a tuple
.
A straightforward interactive method, getUserShift
, is wrapped
around this function to get the user’s choice, which ultimately
returns the same tuple:
def getUserShift(point, prompt, win): #NEW direction selection
'''Return the change in position from the point to a mouse click in win.
First display the prompt string under point.'''
text = Text(Point(point.getX(), 60), prompt)
text.draw(win)
userPt = win.getMouse()
text.undraw()
return getShift(point, userPt)
In the new version of the main driver, bounceBall
, excerpted
below, this interactive setting of (dx, dy) is used. Note the
multiple assignment statement to both dx and dy, set from the tuple
returned from getUserShift
. This shift would generally be much
too much for a single animation step, so the actual values passed
to bounceBall are scaled way down by a factor scale
.
center = Point(win.getWidth()/2, win.getHeight()/2) #NEW central starting point
ball = makeDisk(center, radius, win)
#NEW interactive direction and speed setting
prompt = '''
Click to indicate the direction and
speed of the ball: The further you
click from the ball, the faster it starts.'''
(dx, dy) = getUserShift(center, prompt, win)
scale = 0.01 # to reduce the size of animation steps
bounceInBox(ball, dx*scale, dy*scale, xLow, xHigh, yLow, yHigh, win)
The bounceInBox
method has the same change to the loop as in
the randomCircles.py example. The method then requires the
GraphWin
, win
, as a further parameter, since checkMouse
is a GraphWin
method.
You can look in Idle at the full source code for bounce2.py
if
you like. The changes from bounce1.py
are all marked with a
comment starting with #NEW
, and all the major changes have been
described above.
In the examples so far of the use of checkMouse()
, we have only
used the fact that a point was clicked, not which point. The next
example version, bounce3.py
, does use the location of mouse
clicks that are read with checkMouse()
to change the direction
and speed of the ball. Try it.
This version only slightly modifies the central animation function,
bounceInBox
, but wraps it in another looping function that
makes the direction and speed of the ball change on each mouse
click. Hence the mouse clicks detected in bounceInBox
need to
be remembered and then returned after the main animation loop
finishes. That requires a name, pt
, to be given to the last
mouse click, so it can be remembered. This means modifying the main
animation loop to initialize the variable pt
before the loop
and reset it at the end of the loop, much as in the use of
getMouse() for the interactive polygon creation. That explains the
first three NEW lines and the last two NEW lines in the revised
bounceInBox
:
def bounceInBox(shape, dx, dy, xLow, xHigh, yLow, yHigh, win):
''' Animate a shape moving in jumps (dx, dy), bouncing when
its center reaches the low and high x and y coordinates.
The animation stops when the mouse is clicked, and the
last mouse click is returned.'''
delay = .001
pt = None #NEW
while pt == None: #NEW
shape.move(dx, dy)
center = shape.getCenter()
x = center.getX()
y = center.getY()
isInside = True #NEW
if x < xLow or x > xHigh:
dx = -dx
isInside = False #NEW
if y < yLow or y > yHigh:
dy = -dy
isInside = False #NEW
time.sleep(delay)
if isInside: # NEW don't mess with dx, dy when outside
pt = win.checkMouse() #NEW
return pt #NEW
def moveInBox(shape, stopHeight, xLow, xHigh, yLow, yHigh, win): #NEW
'''Shape bounces in win so its center stays within the low and high
x and y coordinates, and changes direction based on mouse clicks,
terminating when there is a click above stopHeight.'''
scale = 0.01
pt = shape.getCenter() # starts motionless
while pt.getY() < stopHeight:
(dx, dy) = getShift(shape.getCenter(), pt)
pt = bounceInBox(shape, dx*scale, dy*scale,
xLow, xHigh, yLow, yHigh, win)
def makeDisk(center, radius, win):
'''Return a red disk that is drawn in win with given center and radius.'''
disk = Circle(center, radius)
disk.setOutline("red")
disk.setFill("red")
disk.draw(win)
return disk
def getShift(point1, point2):
'''Returns a tuple (dx, dy) which is the shift from point1 to point2.'''
dx = point2.getX() - point1.getX()
dy = point2.getY() - point1.getY()
return (dx, dy)
def bounceBall():
'''Make a ball bounce around the screen, and react to mouse clicks.'''
win = GraphWin('Ball Bounce 3', 290, 290)
win.yUp()
#NEW to mark and label the area where a click stops the program
lineHeight = win.getHeight() - 40
textHeight = win.getHeight() - 20
Line(Point(0, lineHeight), Point(win.getWidth(), lineHeight)).draw(win)
prompt = 'Click above the line to stop\nor below to move toward the click.'
Text(Point(win.getWidth()/2, textHeight), prompt).draw(win)
radius = 10
xLow = radius # center is separated from the wall by the radius at a bounce
xHigh = win.getWidth() - radius
yLow = radius
yHigh = lineHeight - radius #NEW lower top to bouncing limits
center = Point(win.getWidth()/2, lineHeight/2)
ball = makeDisk(center, radius, win)
moveInBox(ball, lineHeight, xLow, xHigh, yLow, yHigh, win) #NEW
win.close()
bounceBall()
I initially made only the changes discussed so far (not the ones
involving the new variable isInside
). The variable isInside
was in response to a bug that I will discuss after introducing the
simple function that wraps around bounceInBox
:
Each time the mouse is clicked, the ball is to switch direction and
move toward the last click, until the stopping condition occurs,
when there is a click above the stop line. This is clearly
repetitive and needs a while loop. The condition is simply to test
the y coordinate of the mouse click against the the height of the
stop line. The body of the loop is very short, since we already
have the utility function getShift
, to figure out (dx, dy)
values.
def moveInBox(shape, stopHeight, xLow, xHigh, yLow, yHigh, win): #NEW
'''Shape bounces in win so its center stays within the low and high
x and y coordinates, and changes direction based on mouse clicks,
terminating when there is a click above stopHeight.'''
scale = 0.01
pt = shape.getCenter() # starts motionless
while pt.getY() < stopHeight:
(dx, dy) = getShift(shape.getCenter(), pt)
pt = bounceInBox(shape, dx*scale, dy*scale,
xLow, xHigh, yLow, yHigh, win)
The variable pt
for the last mouse click needed to be
initialized some way. I chose to make the value be the same as the
initial position of the ball, so both dx and dy are initially 0,
and the ball does not start in motion. (Alternatives are in
Random Start Exercise below.)
I occasionally detected a bug when using the program. The ball would get stuck just outside the boundary and stay there. The fact that it was slightly beyond the boundary was a clue: For simplicity I had cheated, and allowed the ball to go just one animation step beyond the intended boundary. With the speed and small step size this works visually. The original code was sure to make an opposite jump back inside at the next step.
After some thought, I noticed that the initial version of the
bounce3.py code for bounceInBox
broke that assumption. When the
ball was where a bounce-back is required, a mouse click could
change (dx, dy) and mess up the bounce. The idea for a fix is not
to let the user change the direction in the moment when the ball
needs to bounce back.
Neither of the original boundary-checking if
statements, by
itself, always determines if the ball is in the region where it
needs to reverse direction. I dealt with this situation by
introducing a Boolean variable isInside
. It is initially set
as True
, and then either of the if
statements can correct
it to False. Then, at the end of the loop, isInside
is used to
make sure the ball is safely inside the proper region when there is
a check for a new mouse click and a possible user adjustment to
(dx, dy).
3.3.4.1. Exercise Moving Undraw¶
** As discussed above at Where to split the loop,
the basic loop logic
works whether the poly.undraw()
call is at the beginning or end
of the loop. Write a variation makePoly2.py
that makes the code
work the other way, with the poly.undraw()
at the beginning of
the loop. Do not change or move any other statement in the loop.
The new place to cut the loop does affect the code
before and after the loop. In particular, the extra statement
drawing poly
is not needed after the loop is completed. Make
other changes to the surrounding code to make this work. Hints: [4]
3.3.4.2. Make Path Exercise¶
** Write a program that is outwardly very similar to makePoly.py
,
and call it
makePath.py
, with a function pathHere
. The only outward
difference between polyHere
and pathHere
is that while the
first creates a closed polygon, and returns it, and the new one
creates a polygonal path, without the final point being
automatically connected to the first point, and a list of the lines
in the path is returned. Internally the functions are quite
different. The change simplifies some things: no need to undraw
anything in the main loop - just draw the latest segment each time
going from the previous point to the just clicked point. There are
complications however: You do need deal specially with the first
point. It has no previous point to connect to. I suggest you handle
this before the main loop:
If the point is inside the rectangle,
draw the point so it is a visible
guide for the next point. Before returning, undraw
this initial point. (The place on the screen will still be visible
if an initial segment is drawn. If no more points were added, the
screen is left blank, which is the way it should be,
and an empty list of lines should be returned.) You also need
to remember the previous point each time through the main loop.
I suggest you think individually about what should happen if
you stop the drawing when the
first, second or third point is outside the rectangle. Also
test each of those cases after the program is written.
In your main program, call the makePath
function two times.
Use the list of lines returned to loop through
and change the color of all the lines in one
path and the width of the lines in the other path. A portion of a
sample image from this program is shown below.
3.3.4.3. Random Start Exercise¶
* (Optional) I chose to have the ball
start motionless, by making the initial value of pt
(which
determines the initial (dx, dy) ) be the center of the ball. Write
a variation startRandom.py
so pt
is randomly chosen. Also
make the initial location of the ball be random. You can copy the
function getRandomPoint
from bounce1.py.
3.3.4.4. Mad Lib While Exercise¶
** Write a program madlib4.py
that modifies the
getKeys
method of madlib2.py
to use a while
loop. (This
is not an animation program, but this section is where you have had
the most experience with while loops!)
Hints: This is actually the most natural approach. I avoided while
loops initially, when only for
loops had been discussed. In the original approach, however, it is
redundant to find every instance
of '{'
to count the number of repetitions and then find them
all again when extracting the cue keys. A more natural way to
control the loop is a while
loop stopping when there are no
further occurrences of '{'
to find
. This involves some further
adjustments. You must cut the loop in a different place (to end
after searching for '{'
). As discussed before, cutting a loop
in a different place may require changes before and after the loop,
too.
3.3.4.5. Find Hole Game Exercise¶
** Write a graphical game program, findHole.py
,
“Find the Hole”. The program should use a random number generator
to determine a circular “hole”, selecting a point and a perhaps the
radius around that point. These
determine the target and are not revealed to the player initially.
The user is then prompted to click around on the screen to
“find the hidden hole”. You should show the points the user has
tried. Once the user selects a point that is within the chosen
radius of the mystery point, the mystery circle should appear. There
should be a message announcing how many steps it took, and the game
should end.
Hint: you have already seen the code to determine the displacement
(dx, dy) between two points: use the getShift
function in
bounce2.py
. Once you have the displacement (dx, dy) between the
hidden center and the latest mouse click, the distance between the
points is (dx*dx + dy*dy)**0.5
, using the Pythagorean
Theorem of geometry. If this distance is no more than the radius that
you have chosen for the mystery circle, then the user has found the
circle! You can use getShift
as written, or modify it into a
function getDistance
that directly returns the distance between
two points.
Many elaborations on this game are possible! Have fun with it!
3.3.5. Fancier Animation Loop Logic (Optional)¶
The final variation is the example program bounce4.py
, which
has the same outward behavior as bounce3.py, but it illustrates a
different internal design decision. The bounce3.py version has two
levels of while loop in two methods, moveInBox
for mouse clicks
and bounceInBox
for bouncing. The bounce4.py version puts all
the code for changing direction inside the main animation loop in
the old outer function, moveInBox
. There are now three reasons
to adjust (dx, dy): bouncing off the sides, bouncing off the top or
bottom, or a mouse click. That is a simplification and unification
of the logic in one sense. The complication now is that the logic
for determining when to quit is buried deep inside the if
-else
logic, not at the heading of the loop. The test for mouse clicks is
inside the while
loop and further inside another if
statement. The
test of the mouse click may merely lead to a change in (dx, dy), or
is a signal to quit. Here is the revised code, with a discussion
afterward of the return statement:
def moveInBox(shape, stopHeight, xLow, xHigh, yLow, yHigh, win):
''' Animate a shape moving toward any mouse click below stopHeight and
bouncing when its center reaches the low or high x or y coordinates.
The animation stops when the mouse is clicked at stopHeight or above.'''
scale = 0.01
delay = .001
dx = 0 #NEW dx and dy are no longer parameters
dy = 0 #NEW
while True: #NEW exit loop at return statement
center = shape.getCenter()
x = center.getX()
y = center.getY()
isInside = True
if x < xLow or x > xHigh:
dx = -dx
isInside = False
if y < yLow or y > yHigh:
dy = -dy
isInside = False
if isInside:
pt = win.checkMouse()
if pt != None: #NEW dealing with mouse click now here
if pt.getY() < stopHeight: # switch direction
(dx, dy) = getShift(center, pt)
(dx, dy) = (dx*scale, dy*scale)
else: #NEW exit from depths of the loop
return #NEW
shape.move(dx, dy)
time.sleep(delay)
Recall that a return
statement immediately terminates function
execution. In this case the function returns no value, but a bare
return
is legal to force the exit. Since the testing is not
done in the normal while
condition, the while
condition is
set as permanently True
. This is not the most common while
loop pattern! It obscures the loop exit. The choice between the
approach of bounce3.py
and bounce4.py
is a matter of taste in the
given situation.
[1] | “while ___”, “do ___ while”, “repeat while”, “repeat until”, “as long as ___, do”, “keep doing ___ as long as” |
[2] | You will need a loop. You can print/append almost all the numbers in the loop. You are likely to omit one number with just this code, but after looking at what you produce, it is easy to separately include the remaining number. There are several ways to do this. |
[3] | Recall the built-in len function!
It applies to lists. |
[4] | The basic issue is similar to the old version: the undraw is not always needed – at the beginning in this case. In this place it is not needed the first time through the loop. The two basic approaches considered for the previous version still work here: make an extra compensating action outside the loop or break into cases inside the loop. Further hint: It is legal to draw a polygon with an empty vertex list - nothing appears on the screen. |