算法设计

    程序规格是输入炮弹的发射角度、初速度和高度,输出炮弹的射程。 虽然可以利用复杂的数学公式直接算出射程,但我们采用模拟炮弹飞行过程的方法来求射程。所谓模拟炮弹飞行过程,就是从炮弹射出炮口开始,计算炮弹在每一时刻的位置(水 平距离和高度),直至炮弹落地。注意,时间和炮弹飞行轨迹都是连续的量,由于计算机不能 处理连续的数值,所以需要将时间和炮弹飞行轨迹“离散化”,也就是将时间划分成一系列离 散的时段,飞行轨迹也相应地划分成一系列离散的点。

    炮弹在每一时段所处的位置可以利用简单的中学物理知识求得。将炮弹速度分解成水平 分量和垂直分量,则炮弹在水平方向的运动是匀速直线运动(忽略空气阻力),在垂直方向的 运动是加速运动(因为重力的影响,炮弹先向上减速飞行,减到向上速度为 0 后改为自由落 体运动)。算法伪代码如下:

    为了理解此算法,请参看示意图 7.9。

    图 7.9 模拟炮弹飞行的有关数据

    炮弹飞行过程中,水平位置的更新很简单:按照匀速直线运动的规律,每个时段 t 内,

    炮弹都飞行 xv * t 距离,因此炮弹在水平方向从 xpos 运动到了新位置

    炮弹垂直方向位置的变化稍微复杂点:由于重力的影响,炮弹向上速度每秒减少 9.8 米/ 秒,经过时段 t,向上速度变成了

    1. yv1 = yv - 9.8 * t

    而炮弹在时段 t 内垂直方向位移可以用这段时间的平均速度乘 t 来计算,因为时段 t 内的平均 速度为起点速度 yv 与终点速度 yv1 之和的一半,故时段 t 内的垂直方向位移为

    1. (yv + yv1) / 2.0 * t

    最后要说明的是,模拟炮弹飞行的循环语句的条件 y>=0 中之所以用等号,是为了使程 序在初始高度为 h0 = 0 的情况下也能进入循环进行模拟。一旦算出炮弹最新高度小于 0,则 终止循环。

    下面是完整程序:

    【程序 7.4】cball1.py

    1. # -*- coding: cp936 -*-
    2. from math import pi,sin,cos
    3. def main():
    4. angle = input("输入发射角度(度): ")
    5. v = input("输入初速度(米/秒): ")
    6. h0 = input("输入初始高度(米): ")
    7. t = input("输入时间间隔(秒): ")
    8. theta = (angle * pi) / 180.0
    9. xv = v * cos(theta)
    10. yv = v * sin(theta)
    11. xpos = 0
    12. ypos = h0
    13. while ypos >= 0:
    14. xpos = xpos + t * xv
    15. yv1 = yv - t * 9.8
    16. ypos = ypos + t * (yv + yv1) / 2.0
    17. yv = yv1
    18. print "射程: %0.1f 米." % (xpos)

    以下是程序 7.4 的一次执行结果:

    1. 输入初速度(米/秒): 300
    2. 输入初始高度(米): 2
    3. 输入时间间隔(秒): 0.1
    4. 射程: 8522.1 米.

    用写作文打比方的话,程序 7.4 采用的是流水帐式的、毫无章法结构的作文方法,它将所有数据和操作语句全都混在一起。程序虽然不长,却使用了 10 个变量,要想理解这个程序就必须时刻记牢并跟踪这 10 个数据的变化,这对人脑来说是个不小的负担。 模块化程序设计有助于改善程序的结构,增强程序的易理解性。我们利用模块化来重新组织程序 7.3 中的语句,形成一些具有相对独立性的模块(函数)。下面就是炮弹模拟程序的 模块化版本:

    【程序 7.5】cball2.py

    1. # -*- coding: cp936 -*- from math import pi,sin,cos
    2. def getInputs():
    3. a = input("输入发射角度(度): ")
    4. v = input("输入初速度(米/秒): ")
    5. h = input("输入初始高度(米): ")
    6. t = input("输入时间间隔(秒): ") return a,v,h,t
    7. def getXY(v,angle):
    8. theta = (angle * pi) / 180.0
    9. xv = v * cos(theta)
    10. yv = v * sin(theta) return xv,yv
    11. def update(t,xpos,ypos,xv,yv): xpos = xpos + t * xv
    12. yv1 = yv - t * 9.8
    13. ypos = ypos + t * (yv + yv1) / 2.0
    14. yv = yv1
    15. return xpos,ypos,yv
    16. def main():
    17. ypos = h0
    18. while ypos >= 0:
    19. xpos,ypos,yv = update(t,xpos,ypos,xv,yv)
    20. 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 而变的,需要时时更新,这就导致了主循环中的那个复杂、累赘的函数 调用:

    函数作为功能黑盒子,应该提供简明易用的接口,而 update 函数的设计显然不够简明易 用,它需要输入 5 个参数,并输出 3 个返回值。这就像一台设计拙劣的电视机,从机壳内伸 出七八根电线,买回家后需要完成复杂的接线之后才能收看电视。请记住,如果函数接口过 于复杂,往往表明这个函数的设计需要改善。

    最后,我们用 OOP 来编写炮弹模拟程序。炮弹原本是现实世界中的一个对象,传统编 程方法却用 xpos、ypos、xv 和 yv 等四个分离的数据来描述它,这是典型的“只见树木不见 森林”。假如有一个 Projectile 类来描述炮弹对象,有关炮弹的一切信息和行为都封装在这个 类中,那么在主程序中要做的就是创建一个炮弹对象,然后由这个对象自己完成所有的计算 任务,代码形如:

    1. def main():
    2. angle, vel, h0, time = getInputs()
    3. cball = Projectile(angle, vel, h0)
    4. while cball.getY() >= 0:
    5. cball.update(time)
    6. print "射程: %0.1f 米." % (cball.getX())

    这段程序的含义是:首先输入炮弹的初始数据 angle、v、h0 以及计算炮弹飞行位置的时间间 隔 t;然后利用这些初始值创建炮弹对象;接着进入主循环,不断请求炮弹更新其位置,直 至炮弹落地。程序中只用到必不可少的 4 个初始数据,其他数据都隐藏在 Projectile 类当中, 这使得程序逻辑非常清晰、易理解。

    构造器 __init__用于初始化新创建的对象,比如为对象的实例变量赋初值。炮弹对象的实 例变量显然应该包括描述炮弹状态的四个数据:xpos、ypos、xv 和 yv。初始化代码如下:

    1. def __init (self, angle, velocity, height):
    2. self.xpos = 0.0
    3. self.ypos = height
    4. theta = pi * angle / 180.0
    5. self.xv = velocity * cos(theta)
    6. self.yv = velocity * sin(theta)

    注意变量 theta 的用途是临时性的,其值只在此处用到,别处不需要,因此没有必要将 theta 也作为炮弹对象的实例变量,而应作为普通的局部变量。

    方法 getX 和 getY 很简单,分别返回实例变量 self.xpos 和 self.ypos 的当前值即可。

    update 方法是最核心的方法,它的任务是更新炮弹在某个时间间隔后的状态。只需传递 一个时间间隔参数 t 给 update 即可,这比程序 7.5 中的 update 简单多了。代码如下:

    1. def update(self,time):
    2. self.xpos = self.xpos + time * self.xv
    3. yv1 = self.yv - time * 9.8
    4. self.yp = self.yp + t * (self.yv + yv1)/2.0

    注意 yv1 也是一个普通的临时变量,它的值在下一次循环中就是 yv 的值,因此程序中将其值保存到实例变量 self.yv 中。

    至此,我们就完成了 Projectile 类的定义。再添加 getInputs 函数后,就得到完整的面向 对象版本的炮弹模拟程序。

    【程序 7.6】cball3.py

    本程序三种版本的设计思想变迁,可以用图 7.10 来刻划。

    7.2.4 编程实例:模拟炮弹飞行 - 图2

    (a) 非模块化过程 (b) 模块化 (c)面向对象

    图 7.10 炮弹模拟程序不同设计方法的变迁