### 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。

圖 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 來刻劃。

(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 事件
- 參考文獻