More On Flow of Control
3.1.6. Nesting Control-Flow Statements, Part I The power of a language like Python comes
largely from the variety of ways basic statements can be combined. In particular, for and if statements can be nested inside each other’s indented blocks. For example, suppose you want to print only the positive numbers from an arbitrary list of numbers in a function with the following heading. Read the pieces for now.
def printAllPositive(numberList):
’’’Print only the positive numbers in numberList.’’’
For example, suppose numberList is [3, -5, 2, -1, 0, 7]. As a human, who has eyes of amazing capacity, you are drawn immediately to the actual correct numbers, 3, 2, and 7, but clearly a computer doing this systematically will have to check every number. That easily suggests a for-each loop starting
for num in numberList:
3.1. IF STATEMENTS 98
What happens in the body of the loop is not the same thing each time: some get printed, and for those we will want the statement
print(num)
but some do not get printed, so it may at first seem that this is not an appropriate situation for a for-each loop, but in fact, there is a consistent action required: Every number must be tested to see if it should be printed. This suggests an if statement, with the condition num > 0. Try loading into Idle and running the example program onlyPositive.py, whose code is shown below. It ends with a line testing the function:
def printAllPositive(numberList):
’’’Print only the positive numbers in numberList.’’’ for num in numberList:
if num > 0: print(num)
printAllPositive([3, -5, 2, -1, 0, 7])
This idea of nesting if statements enormously expands the possibilities with loops. Now different things can be done at different times in loops, as long as there is a consistent test to allow a choice between the alternatives.
The rest of this section deals with graphical examples.
Run example program bounce1.py. It has a red ball moving and bouncing obliquely off the edges. If you watch several times, you should see that it starts from random locations. Also you can repeat the program from the Shell prompt after you have run the script. For instance, right after running the program, try in the Shell
bounceBall(-3, 1)
The parameters give the amount the shape moves in each animation step. You can try other values in the Shell, preferably with magnitudes less than 10.
For the remainder of the description of this example, read the extracted text pieces.
The animations before this were totally scripted, saying exactly how many moves in which direction, but in this case the direction of motion changes with every bounce. The program has a graphic object shape and the central animation step is
shape.move(dx, dy)
but in this case, dx and dy have to change when the ball gets to a boundary. For instance, imagine the ball getting to the left side as it is moving to the left and up. The bounce obviously alters the horizontal part of the motion, in fact reversing it, but the ball would still continue up. The reversal of the horizontal part of the motion means that the horizontal shift changes direction and therefore its sign:
dx = -dx
but dy does not need to change. This switch does not happen at each animation step, but only when the ball reaches the edge of the window. It happens only some of the time – suggesting an if statement. Still the condition must be determined. Suppose the center of the ball has coordinates (x, y). When x reaches some particular x coordinate, call it xLow, the ball should bounce.
The edge of the window is at coordinate 0, but xLow should not be 0, or the ball would be half way off the screen before bouncing! For the edge of the ball to hit the edge of the screen, the x coordinate of the center must be the length of the radius away, so actually xLow is the radius of the ball.
Animation goes quickly in small steps, so I cheat. I allow the ball to take one (small, quick) step past where it really should go (xLow), and then we reverse it so it comes back to where it belongs. In particular
if x < xLow: dx = -dx
There are similar bounding variables xHigh, yLow and yHigh, all the radius away from the actual edge coordinates, and similar conditions to test for a bounce off each possible edge. Note that whichever edge is hit, one coordinate, either dx or dy, reverses. One way the collection of tests could be written is
if x < xLow: dx = -dx if x > xHigh
dx = -dx if y < yLow: dy = -dy if y > yHigh dy = -dy
This approach would cause there to be some extra testing: If it is true that x < xLow, then it is impossible for it to be true that x > xHigh, so we do not need both tests together. We avoid unnecessary tests with an elif clause (for both x and y):
if x < xLow: dx = -dx elif x > xHigh dx = -dx if y < yLow: dy = -dy elif y > yHigh dy = -dy
Note that the middle if is not changed to an elif, because it is possible for the ball to reach a corner, and need both dx and dy reversed.
The program also uses several accessor methods for graphics objects that we have not used in examples yet. Various graphics objects, like the circle we are using as the shape, know their center point, and it can be accessed with the getCenter() method. (Actually a clone of the point is returned.) Also each coordinate of a Point can be accessed with the getX() and getY() methods.
This explains the new features in the central function defined for bouncing around in a box, bounceInBox. The animation arbitrarily goes on in a simple repeat loop for 600 steps. (A later example will improve this behavior.):
def bounceInBox(shape, dx, dy, xLow, xHigh, yLow, yHigh): ’’’ Animate a shape moving in jumps (dx, dy), bouncing when its center reaches the low and high x and y coordinates. ’’’ delay = .005 for i in range(600): shape.move(dx, dy) center = shape.getCenter() x = center.getX() y = center.getY() if x < xLow: dx = -dx elif x > xHigh: dx = -dx if y < yLow: dy = -dy elif y > yHigh: dy = -dy time.sleep(delay)
The program starts the ball from an arbitrary point inside the allowable rectangular bounds. This is encapsulated in a utility function included in the program, getRandomPoint. The getRandomPoint function uses the randrange function from the module random. Note that in parameters for both the functions range and randrange, the end stated is past the last value actually desired:
def getRandomPoint(xLow, xHigh, yLow, yHigh):
’’’Return a random Point with coordinates in the range specified.’’’ x = random.randrange(xLow, xHigh+1)
3.1. IF STATEMENTS 100
y = random.randrange(yLow, yHigh+1) return Point(x, y)
The full program is listed below, repeating bounceInBox and getRandomPoint for completeness. Several parts that may be useful later, or are easiest to follow as a unit, are separated out as functions. Make sure you see how it all hangs together or ask questions!
’’’
Show a ball bouncing off the sides of the window. ’’’
from graphics import * import time, random
def bounceInBox(shape, dx, dy, xLow, xHigh, yLow, yHigh): ’’’ Animate a shape moving in jumps (dx, dy), bouncing when its center reaches the low and high x and y coordinates. ’’’ delay = .005 for i in range(600): shape.move(dx, dy) center = shape.getCenter() x = center.getX() y = center.getY() if x < xLow: dx = -dx elif x > xHigh: dx = -dx if y < yLow: dy = -dy elif y > yHigh: dy = -dy time.sleep(delay)
def getRandomPoint(xLow, xHigh, yLow, yHigh):
’’’Return a random Point with coordinates in the range specified.’’’ x = random.randrange(xLow, xHigh+1)
y = random.randrange(yLow, yHigh+1) return Point(x, y)
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 bounceBall(dx, dy):
’’’Make a ball bounce around the screen, initially moving by (dx, dy) at each jump.’’’
winWidth = 290 winHeight = 290
win = GraphWin(’Ball Bounce’, winWidth, winHeight) win.setCoords(0, 0, winWidth, winHeight)
radius = 10
xLow = radius # center is separated from the wall by the radius at a bounce xHigh = winWidth - radius
yLow = radius
yHigh = winHeight - radius
center = getRandomPoint(xLow, xHigh, yLow, yHigh) ball = makeDisk(center, radius, win)
bounceInBox(ball, dx, dy, xLow, xHigh, yLow, yHigh) win.close()
bounceBall(3, 5)