Before launching into a detailed discussion of how to write your own classes, let’s take a short detour to see how useful new classes can be.
10.2.1
Program Specification
Suppose we want to write a program that simulates the flight of a cannonball (or any other projectile such as a bullet, baseball or shot put). We are particularly interested in finding out how far the cannonball will travel when fired at various launch angles and initial velocities. The input to the program will be the launch angle (in degrees), the initial velocity (in meters per second) and the initial height (in meters). The output will be the distance that the projectile travels before striking the ground (in meters).
If we ignore the effects of wind resistance and assume that the cannon ball stays close to earth’s surface (i.e., we’re not trying to put it into orbit), this is a relatively simple classical physics problem. The acceleration of gravity near the earth’s surface is about 9.8 meters per second per second. That means if an object is thrown upward at a speed of 20 meters per second, after one second has passed, its upward speed will have slowed to 20 9 8 10 2 meters per second. After another second, the speed will be only 0 4 meters per second, and shortly thereafter it will start coming back down.
For those who know a little bit of calculus, it’s not hard to derive a formula that gives the position of our cannonball at any given moment in its flight. Rather than take the calculus approach, however, our program will use simulation to track the cannonball moment by moment. Using just a bit of simple trigonometry to get started, along with the obvious relationship that the distance an object travels in a given amount of time is equal to its rate times the amount of time (d rt), we can solve this problem algorithmically.
10.2.2
Designing the Program
Let’s start by desigining an algorithm for this problem. Given the problem statement, it’s clear that we need to consider the flight of the cannonball in two dimensions: height, so we know when it hits the ground, and distance, to keep track of how far it goes. We can think of the position of the cannonball as a point xy in a 2D graph where the value of y gives the height and the value of x gives the distance from the starting point.
Our simulation will have to update the position of the cannonball to account for its flight. Suppose the ball starts at position 00, and we want to check its position, say, every tenth of a second. In that interval, it will have moved some distance upward (positive y) and some distance forward (positive x). The exact distance in each dimension is determined by its velocity in that direction.
Separating out the x and y components of the velocity makes the problem easier. Since we are ignoring wind resistance, the x velocity remains constant for the entire flight. However, the y velocity changes over time due to the influence of gravity. In fact, the y velocity will start out being positive and then become negative as the cannonball starts back down.
Given this analysis, it’s pretty clear what our simulation will have to do. Here is a rough outline:
Input the simulation parameters: angle, velocity, height, interval. Calculate the initial position of the cannonball: xpos, ypos
Calculate the initial velocities of the cannonball: xvel, yvel While the cannonball is still flying:
update the values of xpos, ypos, and yvel for interval seconds further into the flight
Output the distance traveled as xpos
Let’s turn this into a program using stepwise refinement.
The first line of the algorithm is straightforward. We just need an appropriate sequence of input state- ments. Here’s a start:
def main():
angle = input("Enter the launch angle (in degrees): ") vel = input("Enter the initial velocity (in meters/sec): ")
h0 = input("Enter the initial height (in meters): ")
time = input("Enter the time interval between position calculations: ")
Calculating the initial position for the cannonball is also easy. It will start at distance 0 and heighth0. We just need a couple assignment statements.
xpos = 0.0 ypos = h0
Next we need to calculate the x and y components of the initial velocity. We’ll need a little high-school trigonometry. (See, they told you you’d use that some day.) If we consider the initial velocity as consisting of some amount of change in y and and some amount of change in x, then these three components (velocity, x- velocity and y-velocity) form a right triangle. Figure 10.1 illustrates the situation. If we know the magnitude of the velocity and the launch angle (labeled theta, because the Greek letterθ is often used as the measure of angles), we can easily calculate the magnitude of xvel by the equation xvel velocity costheta. A similar
formula (using sintheta) provides yvel.
yvel = velocity * sin(theta)
velocity
theta
xvel = velocity * cos(theta)
Figure 10.1: Finding the x and y components of velocity.
Even if you don’t completely understand the trigonometry, the important thing is that we can translate these formulas into Python code. There’s still one subtle issue to consider. Our input angle is in degrees, and the Pythonmathlibrary uses radian measures. We’ll have to convert our angle before applying the formulas. There are 2πradians in a circle (360 degrees); so theta π
angle
180 . These three formulas give us the code for computing the initial velocities:
theta = math.pi * angle / 180.0 xvel = velocity * math.cos(theta) yvel = velocity * math.sin(theta)
That brings us to the main loop in our program. We want to keep updating the position and velocity of the cannonball until it reaches the ground. We can do this by examining the value ofypos.
while ypos >= 0.0:
I used
as the relationship so that we can start with the cannon ball on the ground (= 0) and still get the loop going. The loop will quit as soon as the value ofyposdips just below 0, indicating the cannonball has embedded itself slightly in the ground.
Now we arrive at the crux of the simulation. Each time we go through the loop, we want to update the state of the cannonball to move ittimeseconds farther in its flight. Let’s start by considering movement in the horizontal direction. Since our specification says that we can ignore wind resistance, the horizontal speed of the cannonball will remain constant and is given by the value ofxvel.
As a concrete example, suppose the ball is traveling at 30 meters per second and is currently 50 meters from the firing point. In another second, it will go 30 more meters and be 80 meters from the firing point. If the interval is only 0.1 second (rather than a full second), then the cannonball will only fly another 0130 3 meters and be at a distance of 53 meters. You can see that the new distance traveled is always given bytime * xvel. To update the horizontal position, we need just one statement.
The situation for the vertical component is slightly more complicated, since gravity causes the y-velocity to change over time. Each second,yvelmust decrease by 9.8 meters per second, the acceleration of gravity. In 0.1 seconds the velocity will decrease by 0 1 9 8 0 98 meters per second. The new velocity at the end of the interval is calculated as
yvel1 = yvel - time * 9.8
To calculate how far the cannonball travels during this interval, we need to know its average vertical velocity. Since the acceleration due to gravity is constant, the average velocity will just be the average of the starting and ending velocities: (yvel+yvel1)/2.0. Multiplying this average velocity by the amount of time in the interval gives us the change in height.
Here is the completed loop:
while yvel >= 0.0:
xpos = xpos + time * xvel yvel1 = yvel - time * 9.8
ypos = ypos + time * (yvel + yvel1)/2.0 yvel = yvel1
Notice how the velocity at the end of the time interval is first stored in the temporary variableyvel1. This is done to preserve the initialyvelso that the average velocity can be computed from the two values. Finally, the value ofyvelis assigned its value at the end of the loop. This represents the correct vertical velocity of the cannonball at the end of the interval.
The last step of our program simply outputs the distance traveled. Adding this step gives us the complete program.
# cball1.py
from math import pi, sin, cos def main():
angle = input("Enter the launch angle (in degrees): ") vel = input("Enter the initial velocity (in meters/sec): ") h0 = input("Enter the initial height (in meters): ")
time = input("Enter the time interval between position calculations: ") # convert angle to radians
theta = (angle * pi)/180.0
# set the intial position and velocities in x and y directions xpos = 0
ypos = h0
xvel = vel * cos(theta) yvel = vel * sin(theta)
# loop until the ball hits the ground while ypos >= 0:
# calculate position and velocity in time seconds xpos = xpos + time * xvel
yvel1 = yvel - time * 9.8
ypos = ypos + time * (yvel + yvel1)/2.0 yvel = yvel1
10.2.3
Modularizing the Program
You may have noticed during the design discussion that I employed stepwise refinement (top-down design) to develop the program, but I did not divide the program into separate functions. We are going to modularize the program in two different ways. First, we’ll use functions (a la top-down design).
While the final program is not too long, it is fairly complex for its length. One cause of the complexity is that it uses ten variables, and that is a lot for the reader to keep track of. Let’s try dividing the program into functional pieces to see if that helps. Here’s a version of the main algorithm using helper functions:
def main():
angle, vel, h0, time = getInputs() xpos, ypos = 0, h0
xvel, yvel = getXYComponents(velocity, angle) while ypos >= 0:
xpos, ypos, yvel = updateCannonBall(time, xpos, ypos, xvel, yvel) print "\nDistance traveled: %0.1f meters." % (xpos)
It should be obvious what each of these functions does based on their names and the original program code. You might take a couple of minutes to code up the three helper functions.
This second version of the main algorithm is certainly more concise. The number of variables has been reduced to eight, sincethetaandyvel1have been eliminated from the main algorithm. Do you see where they went? The value ofthetais only needed locally inside ofgetXYComponents. Similarly,yvel1is now local toupdateCannonBall. Being able to hide some of the intermediate variables is a major benefit of the separation of concerns provided by top-down design.
Even this version seems overly complicated. Look especially at the loop. Keeping track of the state of the cannonball requires four pieces of information, three of which must change from moment to moment. All four variables along with the value oftimeare needed to compute the new values of the three that change. That results in an ugly function call having five parameters and three return values. An explosion of parameters is often an indication that there might be a better way to organize a program. Let’s try another approach.
The original problem specification itself suggests a better way to look at the variables in our program. There is a single real-world cannonball object, but describing it in the current program requires four pieces of information: xpos,ypos,xvelandyvel. Suppose we had aProjectileclass that “understood” the physics of objects like cannonballs. Using such a class, we could express the main algorithm in terms of creating and updating a suitable object stored in a single variable. Using this object-based approach, we might writemainlike this.
def main():
angle, vel, h0, time = getInputs() cball = Projectile(angle, vel, h0) while cball.getY() >= 0:
cball.update(time)
print "\nDistance traveled: %0.1f meters." % (cball.getX())
Obviously, this is a much simpler and more direct expression of the algorithm. The initial values of
angle,velandh0are used as parameters to create aProjectilecalledcball. Each time through
the loop,cball is asked to update its state to account fortime. We can get the position of cballat any moment by using itsgetXandgetYmethods. To make this work, we just need to define a suitable
Projectileclass that implements the methodsupdate,getX, andgetY.