### 7.2.4 编程实例:模拟炮弹飞行
本节讨论一个模拟炮弹飞行的程序的设计。我们采用三种设计方法,得到三个版本的程序。通过比较各个版本的差别,可以看出 OOP 与传统的面向过程编程相比具有明显优点。
算法设计
程序规格是输入炮弹的发射角度、初速度和高度,输出炮弹的射程。 虽然可以利用复杂的数学公式直接算出射程,但我们采用模拟炮弹飞行过程的方法来求射程。所谓模拟炮弹飞行过程,就是从炮弹射出炮口开始,计算炮弹在每一时刻的位置(水 平距离和高度),直至炮弹落地。注意,时间和炮弹飞行轨迹都是连续的量,由于计算机不能 处理连续的数值,所以需要将时间和炮弹飞行轨迹“离散化”,也就是将时间划分成一系列离 散的时段,飞行轨迹也相应地划分成一系列离散的点。
炮弹在每一时段所处的位置可以利用简单的中学物理知识求得。将炮弹速度分解成水平 分量和垂直分量,则炮弹在水平方向的运动是匀速直线运动(忽略空气阻力),在垂直方向的 运动是加速运动(因为重力的影响,炮弹先向上减速飞行,减到向上速度为 0 后改为自由落 体运动)。算法伪代码如下:
```
算法:模拟炮弹飞行。
输入:角度 angle(度)、初速度 v(米/秒)、高度 h0(米)、时间间隔 t(秒)
输出:射程(米)
计算初速度分量:先将 angle 换算成弧度单位的 theta,再计算
xv = v * cos(theta),yv = v * sin(theta)
初始位置:(xpos,ypos) = (0,h0)
当炮弹还未落地(即 ypos >= 0.0):
更新炮弹在下一时段的位置(xpos,ypos)和垂直速度分量 yv
输出 xpos
```
> ① Java 和 C++中使用的是“this”。
为了理解此算法,请参看示意图 7.9。
![](https://box.kancloud.cn/2016-02-22_56cafce446e7c.png)
图 7.9 模拟炮弹飞行的有关数据
炮弹飞行过程中,水平位置的更新很简单:按照匀速直线运动的规律,每个时段 t 内,
炮弹都飞行 xv * t 距离,因此炮弹在水平方向从 xpos 运动到了新位置
```
xpos = xpos + xv * t
```
炮弹垂直方向位置的变化稍微复杂点:由于重力的影响,炮弹向上速度每秒减少 9.8 米/ 秒,经过时段 t,向上速度变成了
```
yv1 = yv - 9.8 * t
```
而炮弹在时段 t 内垂直方向位移可以用这段时间的平均速度乘 t 来计算,因为时段 t 内的平均 速度为起点速度 yv 与终点速度 yv1 之和的一半,故时段 t 内的垂直方向位移为
```
(yv + yv1) / 2.0 * t
```
于是,经过时段 t 后,炮弹在垂直方向的新位置为
```
ypos = ypos + (yv + yv1) / 2.0 * t
```
最后要说明的是,模拟炮弹飞行的循环语句的条件 y>=0 中之所以用等号,是为了使程 序在初始高度为 h0 = 0 的情况下也能进入循环进行模拟。一旦算出炮弹最新高度小于 0,则 终止循环。
下面是完整程序:
【程序 7.4】cball1.py
```
# -*- coding: cp936 -*-
from math import pi,sin,cos
def main():
angle = input("输入发射角度(度): ")
v = input("输入初速度(米/秒): ")
h0 = input("输入初始高度(米): ")
t = input("输入时间间隔(秒): ")
theta = (angle * pi) / 180.0
xv = v * cos(theta)
yv = v * sin(theta)
xpos = 0
ypos = h0
while ypos >= 0:
xpos = xpos + t * xv
yv1 = yv - t * 9.8
ypos = ypos + t * (yv + yv1) / 2.0
yv = yv1
print "射程: %0.1f 米." % (xpos)
main()
```
以下是程序 7.4 的一次执行结果:
```
输入发射角度(度): 56
输入初速度(米/秒): 300
输入初始高度(米): 2
输入时间间隔(秒): 0.1
射程: 8522.1 米.
```
用写作文打比方的话,程序 7.4 采用的是流水帐式的、毫无章法结构的作文方法,它将所有数据和操作语句全都混在一起。程序虽然不长,却使用了 10 个变量,要想理解这个程序就必须时刻记牢并跟踪这 10 个数据的变化,这对人脑来说是个不小的负担。 模块化程序设计有助于改善程序的结构,增强程序的易理解性。我们利用模块化来重新组织程序 7.3 中的语句,形成一些具有相对独立性的模块(函数)。下面就是炮弹模拟程序的 模块化版本:
【程序 7.5】cball2.py
```
# -*- coding: cp936 -*- from math import pi,sin,cos
def getInputs():
a = input("输入发射角度(度): ")
v = input("输入初速度(米/秒): ")
h = input("输入初始高度(米): ")
t = input("输入时间间隔(秒): ") return a,v,h,t
def getXY(v,angle):
theta = (angle * pi) / 180.0
xv = v * cos(theta)
yv = v * sin(theta) return xv,yv
def update(t,xpos,ypos,xv,yv): xpos = xpos + t * xv
yv1 = yv - t * 9.8
ypos = ypos + t * (yv + yv1) / 2.0
yv = yv1
return xpos,ypos,yv
def main():
angle, v, h0, t = getInputs()
xv, yv = getXY(v,angle) xpos = 0
ypos = h0
while ypos >= 0:
xpos,ypos,yv = update(t,xpos,ypos,xv,yv)
print "射程: %0.1f 米." % (xpos)
```
与程序 7.4 相比,程序 7.5 的主程序 main 显得非常简洁、容易理解。main 中用到的变量 从 10 个减到 8 个,少掉的两个变量是 theta 和 yv1。变量 theta 存储的是以弧度为单位的发射 角度,它是为了符合 math 库中三角函数的用法而临时创建的中间数据,对程序来说既不是输 入数据,又不是输出数据,也不是贯穿算法始终的关键数据。因此,将 theta 隐藏在用到它的 函数 getXY 中,是符合它的“跑龙套”身份的做法。基于同样的理由,yv1 也被隐藏在了函 数 update 中。
然而,尽管模块化编程改善了程序的结构,使程序易读易理解,但程序 7.5 的主程序仍 然比较复杂。为了描述炮弹的飞行状态,需要 xpos、ypos、xv 和 yv 等 4 个数据,其中 xpos、 ypos 和 yv 是随时间 t 而变的,需要时时更新,这就导致了主循环中的那个复杂、累赘的函数 调用:
```
xpos,ypos,yv = update(t,xpos,ypos,xv,yv)
```
函数作为功能黑盒子,应该提供简明易用的接口,而 update 函数的设计显然不够简明易 用,它需要输入 5 个参数,并输出 3 个返回值。这就像一台设计拙劣的电视机,从机壳内伸 出七八根电线,买回家后需要完成复杂的接线之后才能收看电视。请记住,如果函数接口过 于复杂,往往表明这个函数的设计需要改善。
最后,我们用 OOP 来编写炮弹模拟程序。炮弹原本是现实世界中的一个对象,传统编 程方法却用 xpos、ypos、xv 和 yv 等四个分离的数据来描述它,这是典型的“只见树木不见 森林”。假如有一个 Projectile 类来描述炮弹对象,有关炮弹的一切信息和行为都封装在这个 类中,那么在主程序中要做的就是创建一个炮弹对象,然后由这个对象自己完成所有的计算 任务,代码形如:
```
def main():
angle, vel, h0, time = getInputs()
cball = Projectile(angle, vel, h0)
while cball.getY() >= 0:
cball.update(time)
print "射程: %0.1f 米." % (cball.getX())
```
这段程序的含义是:首先输入炮弹的初始数据 angle、v、h0 以及计算炮弹飞行位置的时间间 隔 t;然后利用这些初始值创建炮弹对象;接着进入主循环,不断请求炮弹更新其位置,直 至炮弹落地。程序中只用到必不可少的 4 个初始数据,其他数据都隐藏在 Projectile 类当中, 这使得程序逻辑非常清晰、易理解。
当然,主程序之所以简单,是因为复杂性都被隐藏在类当中了。下面来考虑 Projectile 类 的定义。前面主程序中实际上已经提出了对类的要求,即类中必须实现 update、getX 和 getY 方法。此外,还必须定义类的构造器。
构造器 \_\_init\_\_用于初始化新创建的对象,比如为对象的实例变量赋初值。炮弹对象的实 例变量显然应该包括描述炮弹状态的四个数据:xpos、ypos、xv 和 yv。初始化代码如下:
```
def __init (self, angle, velocity, height):
self.xpos = 0.0
self.ypos = height
theta = pi * angle / 180.0
self.xv = velocity * cos(theta)
self.yv = velocity * sin(theta)
```
注意变量 theta 的用途是临时性的,其值只在此处用到,别处不需要,因此没有必要将 theta 也作为炮弹对象的实例变量,而应作为普通的局部变量。
方法 getX 和 getY 很简单,分别返回实例变量 self.xpos 和 self.ypos 的当前值即可。
update 方法是最核心的方法,它的任务是更新炮弹在某个时间间隔后的状态。只需传递 一个时间间隔参数 t 给 update 即可,这比程序 7.5 中的 update 简单多了。代码如下:
```
def update(self,time):
self.xpos = self.xpos + time * self.xv
yv1 = self.yv - time * 9.8
self.yp = self.yp + t * (self.yv + yv1)/2.0
self.yv = yv1
```
注意 yv1 也是一个普通的临时变量,它的值在下一次循环中就是 yv 的值,因此程序中将其值保存到实例变量 self.yv 中。
至此,我们就完成了 Projectile 类的定义。再添加 getInputs 函数后,就得到完整的面向 对象版本的炮弹模拟程序。
【程序 7.6】cball3.py
```
from math import pi,sin,cos
class Projectile:
def __init__ (self,angle,velocity,height):
self.xpos = 0.0
self.ypos = height
theta = pi * angle / 180.0
self.xv = velocity * cos(theta)
self.yv = velocity * sin(theta)
def update(self, time):
self.xpos = self.xpos + time *
self.xv yv1 = self.yv - 9.8 * time
self.ypos = self.ypos + time * (self.yv + yv1) / 2.0
self.yv = yv1
def getX(self):
return self.xpos
def getY(self):
return self.ypos
def getInputs():
a = input("输入发射角度(度): ")
v = input("输入初速度(米/秒): ")
h = input("输入初始高度(米): ")
t = input("输入时间间隔(秒): ") return a,v,h,t
def main():
angle,v,h0,t = getInputs()
cball = Projectile(angle,v,h0)
while cball.getY() >= 0:
cball.update(t)
print "射程: %0.1f 米." % (cball.getX())
```
本程序三种版本的设计思想变迁,可以用图 7.10 来刻划。
![](https://box.kancloud.cn/2016-02-22_56cafce45c541.png)
(a) 非模块化过程 (b) 模块化 (c)面向对象
图 7.10 炮弹模拟程序不同设计方法的变迁
- 前言
- 第 1 章 计算与计算思维
- 1.1 什么是计算?
- 1.1.1 计算机与计算
- 1.1.2 计算机语言
- 1.1.3 算法
- 1.1.4 实现
- 1.2 什么是计算思维?
- 1.2.1 计算思维的基本原则
- 1.2.2 计算思维的具体例子
- 1.2.3 日常生活中的计算思维
- 1.2.4 计算思维对其他学科的影响
- 1.3 初识 Python
- 1.3.1 Python 简介
- 1.3.2 第一个程序
- 1.3.3 程序的执行方式
- 1.3.4 Python 语言的基本成分
- 1.4 程序排错
- 1.5 练习
- 第 2 章 用数据表示现实世界
- 2.1 数据和数据类型
- 2.1.1 数据是对现实的抽象
- 2.1.1 常量与变量
- 2.1.2 数据类型
- 2.1.3 Python 的动态类型*
- 2.2 数值类型
- 2.2.1 整数类型 int
- 2.2.2 长整数类型 long
- 2.2.3 浮点数类型 float
- 2.2.4 数学库模块 math
- 2.2.5 复数类型 complex*
- 2.3 字符串类型 str
- 2.3.1 字符串类型的字面值形式
- 2.3.2 字符串类型的操作
- 2.3.3 字符的机内表示
- 2.3.4 字符串类型与其他类型的转换
- 2.3.5 字符串库 string
- 2.4 布尔类型 bool
- 2.4.1 关系运算
- 2.4.2 逻辑运算
- 2.4.3 布尔代数运算定律*
- 2.4.4 Python 中真假的表示与计算*
- 2.5 列表和元组类型
- 2.5.1 列表类型 list
- 2.5.2 元组类型 tuple
- 2.6 数据的输入和输出
- 2.6.1 数据的输入
- 2.6.2 数据的输出
- 2.6.3 格式化输出
- 2.7 编程案例:查找问题
- 2.8 练习
- 第 3 章 数据处理的流程控制
- 3.1 顺序控制结构
- 3.2 分支控制结构
- 3.2.1 单分支结构
- 3.2.2 两路分支结构
- 3.2.3 多路分支结构
- 3.3 异常处理
- 3.3.1 传统的错误检测方法
- 3.3.2 传统错误检测方法的缺点
- 3.3.3 异常处理机制
- 3.4 循环控制结构
- 3.4.1 for 循环
- 3.4.2 while 循环
- 3.4.3 循环的非正常中断
- 3.4.4 嵌套循环
- 3.5 结构化程序设计
- 3.5.1 程序开发过程
- 3.5.2 结构化程序设计的基本内容
- 3.6 编程案例:如何求 n 个数据的最大值?
- 3.6.1 几种解题策略
- 3.6.2 经验总结
- 3.7 Python 布尔表达式用作控制结构*
- 3.8 练习
- 第 4 章 模块化编程
- 4.1 模块化编程基本概念
- 4.1.1 模块化设计概述
- 4.1.2 模块化编程
- 4.1.3 编程语言对模块化编程的支持
- 4.2 Python 语言中的函数
- 4.2.1 用函数减少重复代码 首先看一个简单的用字符画一棵树的程序:
- 4.2.2 用函数改善程序结构
- 4.2.3 用函数增强程序的通用性
- 4.2.4 小结:函数的定义与调用
- 4.2.5 变量的作用域
- 4.2.6 函数的返回值
- 4.3 自顶向下设计
- 4.3.1 顶层设计
- 4.3.2 第二层设计
- 4.3.3 第三层设计
- 4.3.4 第四层设计
- 4.3.5 自底向上实现与单元测试
- 4.3.6 开发过程小结
- 4.4 Python 模块*
- 4.4.1 模块的创建和使用
- 4.4.2 Python 程序架构
- 4.4.3 标准库模块
- 4.4.4 模块的有条件执行
- 4.5 练习
- 第 5 章 图形编程
- 5.1 概述
- 5.1.1 计算可视化
- 5.1.2 图形是复杂数据
- 5.1.3 用对象表示复杂数据
- 5.2 Tkinter 图形编程
- 5.2.1 导入模块及创建根窗口
- 5.2.2 创建画布
- 5.2.3 在画布上绘图
- 5.2.4 图形的事件处理
- 5.3 编程案例
- 5.3.1 统计图表
- 5.3.2 计算机动画
- 5.4 软件的层次化设计:一个案例
- 5.4.1 层次化体系结构
- 5.4.2 案例:图形库 graphics
- 5.4.3 graphics 与面向对象
- 5.5 练习
- 第 6 章 大量数据的表示和处理
- 6.1 概述
- 6.2 有序的数据集合体
- 6.2.1 字符串
- 6.2.2 列表
- 6.2.3 元组
- 6.3 无序的数据集合体
- 6.3.1 集合
- 6.3.2 字典
- 6.4 文件
- 6.4.1 文件的基本概念
- 6.4.2 文件操作
- 6.4.3 编程案例:文本文件分析
- 6.4.4 缓冲
- 6.4.5 二进制文件与随机存取*
- 6.5 几种高级数据结构*
- 6.5.1 链表
- 6.5.2 堆栈
- 6.5.3 队列
- 6.6 练习
- 第 7 章 面向对象思想与编程
- 7.1 数据与操作:两种观点
- 7.1.1 面向过程观点
- 7.1.2 面向对象观点
- 7.1.3 类是类型概念的发展
- 7.2 面向对象编程
- 7.2.1 类的定义
- 7.2.2 对象的创建
- 7.2.3 对象方法的调用
- 7.2.4 编程实例:模拟炮弹飞行
- 7.2.5 类与模块化
- 7.2.6 对象的集合体
- 7.3 超类与子类*
- 7.3.1 继承
- 7.3.2 覆写
- 7.3.3 多态性
- 7.4 面向对象设计*
- 7.5 练习
- 第 8 章 图形用户界面
- 8.1 图形用户界面概述
- 8.1.1 程序的用户界面
- 8.1.2 图形界面的组成
- 8.1.3 事件驱动
- 8.2 GUI 编程
- 8.2.1 UI 编程概述
- 8.2.2 初识 Tkinter
- 8.2.3 常见 GUI 构件的用法
- 8.2.4 布局
- 8.2.5 对话框*
- 8.3 Tkinter 事件驱动编程
- 8.3.1 事件和事件对象
- 8.3.2 事件处理
- 8.4 模型-视图设计方法
- 8.4.1 将 GUI 应用程序封装成对象
- 8.4.2 模型与视图
- 8.4.3 编程案例:汇率换算器
- 8.5 练习
- 第 9 章 模拟与并发
- 9.1 模拟
- 9.1.1 计算机建模
- 9.1.2 随机问题的建模与模拟
- 9.1.3 编程案例:乒乓球比赛模拟
- 9.2 原型法
- 9.3 并行计算*
- 9.3.1 串行、并发与并行
- 9.3.2 进程与线程
- 9.3.3 多线程编程的应用
- 9.3.4 Python 多线程编程
- 9.3.5 小结
- 9.4 练习
- 第 10 章 算法设计和分析
- 10.1 枚举法
- 10.2 递归
- 10.3 分治法
- 10.4 贪心法
- 10.5 算法分析
- 10.5.1 算法复杂度
- 10.5.2 算法分析实例
- 10.6 不可计算的问题
- 10.7 练习
- 第 11 章 计算+X
- 11.1 计算数学
- 11.2 生物信息学
- 11.3 计算物理学
- 11.4 计算化学
- 11.5 计算经济学
- 11.6 练习
- 附录
- 1 Python 异常处理参考
- 2 Tkinter 画布方法
- 3 Tkinter 编程参考
- 3.1 构件属性值的设置
- 3.2 构件的标准属性
- 3.3 各种构件的属性
- 3.4 对话框
- 3.5 事件
- 参考文献