Python 程式設計

第 15 章    物件與類別

(1) 物件簡介

∗ 物件 (Object):

▸ 物件用來描述一個物體、一件事、或一個人,物件內含:

✶ 資訊
✶ 可執行的操作,用來處理資訊

▸ 以物件的概念來解決問題,稱為「物件導向程式設計」(Object-oriented programming, OOP)

▸ 支援物件的程式語言稱為「物件導向程式語言」(Object-oriented programming language, OOPL),Python 屬此類語言

(2) 類別

∗ 類別 (Class)

▸ 每個物件都是某個類別 (Class) 所產生的一個實例 (Instance),就好像工廠裡一個模具可以產生許多相同規格的產品一般:模具就是類別,產品就是實例

▸ 類別裡可包含屬性 (Attribute) 及方法 (Method)

✶ 屬性:儲存物件的資訊,也就是變數
✶ 方法:操作物件的資訊,也就是函式

∗ 建立類別的語法

▸ 一般類別:

class <className>:
    ...
✶ 例如:圓 (Circle)
ch15/circle.py
class Circle:
    cx = 0
    cy = 0
    radius = 0

    def setColor(self, color):
        self.color = color
# 類別名稱為 Circle (通常使用大寫)
# 定義三個屬性:圓心位置座標 (cx, cy) 及半徑 (r),預設值都是 0
# 定義一個方法 set_color():用來設定顏色,有兩個參數 selfcolor
* 類別方法的第一個參數一定是 self ( 為什麼一定要有這個參數?)
* 在方法中存取物件屬性需在屬性名稱前加上 self.,此例中只要執行 set_color() 方法,就會動態新增一個 color 屬性
* 在方法中沒有 self. 前置的變數是區域變數,方法執行完畢即不存在,例如 set_color() 方法中的紅色 color
class Circle:
    def set_color(self, color):
        self.color = color

▸ 繼承類別 (Inheritance):在類別名稱後加小括號,內含所繼承類別的名稱 (稱為「父類別」,Parent class)

class <className>(<parentClass>):
    ...
✶ 例如:Ball 類別繼承 Circle 類別
class Ball(Circle):
    cz = 0
# 類別名稱為 Ball,繼承自 Circle 類別, 因此擁有該類別裡的所有屬性與方法:cx, cy, radius, setColor()
# 本類別另外再定義一個屬性:球心第三維座標 cz

(3) 產生物件

∗ 物件的產生

▸ 語法:在類別名稱後加上一對小括號,並指派給一個變數即可產生一個實例

<variableName> = <className>()

<className>() 稱為建構子 (Constructor),用來產生新的實例

▸ 以 CircleBall 類別為例, 以下分別產生一個名為 circleball 的實例:

circle = Circle()
ball = Ball()

∗ 存取屬性與執行方法

▸ 語法:都是利用點號

<object>.<attribute>     # 存取屬性
<object>.<method>()      # 執行方法

▸ 例例如:產生一個圓,列印半徑,設定圓心的 cx 座標為並列印,然後設定為紅色並列印

ch15/circle.py
class Circle:
    ...

    def setColor(self, color):
        self.color = color

circle = Circle()
print(circle.radius)
circle.cx = 20
print(circle.cx)
circle.setColor('red')
print(circle.color)
0
20
red

▸ 練習 1

∗ 物件的初始設定

▸ 在產生物件時可以一併設定屬性:在類別裡加上 __init__() 初始方法,在產生實例時就會自動執行

class <className>:

    def __init__(self, <parameters>):
        ...
self 之後的參數即為呼叫建構子時所傳入的參數
__init__ 名稱是固定,不可以更改為其他名稱

▸ 產生物件並傳入初始值之語法:

<variableName> = <className>(<parameters>)

▸ 以 Circle 為例,在類別中加入 __init__() 方法, 用來設定圓心位置及半徑的初始值:

ch15/circle.py
class Circle:

    def __init__(self, cx, cy, radius):
        self.cx = cx
        self.cy = cy
        self.radius = radius

    def setColor(self, color):
        self.color = color

    ...
__init__() 方法有四個參數,其中 self 是必須的,另外三個 (cx, cy, radius) 是輸入參數,用來設定屬性的初始值
✶ 有了初始方法後,在產生物件時就需要提供建構子必要的參數 (不含 self),例如:
circle = Circle(10, 20, 30)     # 產生一個圓心位置在 (10, 20),且半徑為 30 的圓
✶ 印出屬性值:
circle.py
class Circle:
    ...

circle = Circle(10, 20, 30)
print(circle.cx)
print(circle.cy)
print(circle.radius)
10
20
30

▸ 練習 2

(4) 程序式設計與物件導向設計的比較:球的碰撞

∗ 程序式設計 (Procedural design)

def main():
    # 建立球的資料
    numBalls = 30
    xs = []    # 位置 x 座標
    ys = []    # 位置 y 座標
    rs = []    # 半徑
    cs = []    # 顏色
    ms = []    # 質量
    es = []    # 彈性係數
    ...       # 其他特性

    for i in range(numBalls):   
        xs.append(...)
        ys.append(...)
        rs.append(...)
        cs.append(...)
        ms.append(...)
        es.append(...)
        ...

    # 處理碰撞
    for i in range(numBalls-1):
        for j in range(i+1, numBalls):
            collision(xs[i], ys[i], xs[j], ys[j], rs[i], rs[j], ms[i], ms[j], es[i], es[j] ...)


def collision(x1, y1, x2, y2, r1, r2, m1, m2, e1, e2...):
    ...

▸ 將所有球的資料以串列表示,資料分散且紛亂

▸ 處理球的碰撞時,需將所有資料以參數傳遞,資料冗長

∗ 物件導向設計 (Object-oriented design)

# 球類別
class Ball:
   def __init__(self, x, y, r, c, m, e, ...):
       self.x = x
       self.y = y
       self.r = r
       self.c = c
       self.m = m
       self.e = e
       ...


def main():
    # 建立球的資料
    numBalls = 30
    balls = []
    for i in range(numBalls):   
        balls.append(Ball(...))

    # 處理碰撞
    for i in range(numBalls-1):
        for j in range(i+1, numBalls):
            collision(balls[i], balls[j])


def collision(b1, b2):
    ...

▸ 每個球的資料均儲存在物件本身,程式只要傳遞球物件,所有資料就帶著走,存取非常方便,程式看起來也簡潔乾淨許多

(5) 範例:模擬砲彈飛行路線

∗ 程式開發流程:

1. 定義問題

2. 分析問題並推導出解決方案

3. 依據解決方案撰寫第一版程式

4. 第二版程式:模組化

5. 第三版程式:物件化

∗ 程式規格:模擬砲彈的飛行路線

▸ 輸入:砲彈發射角度 (度,degree),初始速度 (公尺/秒,m/s),初始高度 (公尺,m)

▸ 輸出:砲彈落地的距離 (m)

∗ 問題分析

▸ 假設忽略風阻,地心引力加速度為 9.8 m/s,亦即如果將一個物體以 20 m/s 的速度垂直向上拋出, 一秒鐘之後,它的向上速度將減為 20 - 9.8 = 10.2 m/s,再過一秒則減為 10.2 - 9.8 = 0.4 m/s, 接著它很快的就要向下墜落了 (當向上速度為0時)

▸ 可以利用微積分概念來計算在某個時間點物體的位置,本程式不用微積分, 而是利用模擬的方式來一點一點的追蹤砲彈

▸ 計算砲彈的飛行需考慮兩項資料:

✶ 高度 (Height):如此才能知道何時落地
✶ 距離 (Distance):以便追蹤砲彈飛了多遠

▸ 砲彈的位置以二維資料表示:(px, py)px 為距離 (預設為 0),py 為高度

✶ 每個時間間隔追蹤一次砲彈位置:在此間隔中,砲彈向上移動了一些位置到達 py, 並且向前移動了一些位置到達 px
✶ 因為忽略風阻,因此 x 方向的移動速度是常數
✶ y 方向的移動速度由地心引力控制:向上飛行為正數,向下墜落則為負數

▸ 解決方案:虛擬程式碼

Input the simulation parameters: angle, velocity, height, interval
Set the initial position of the cannonball: px, py
Calculate the initial velocity of the cannonball: vx, vy
While the cannonball is still flying:
    update the values of px, py, and vy for an interval
output the distance traveled as px

▸ 更新各項資料所需要使用的知識:

✶ 速度分解:三角學 (你看,以前的老師不是說以後一定會用到嗎?)
cannonball.png
✶ 物理學:距離等於速度乘以時間 d = vt (你看,以前學的一定會用到!)
# 水平方向:速度不變,距離變化:px2 = px1 + tvx
# 垂直方向:速度變化:vy2 = vy1 - 9.8t, 距離變化:py2 = py1 + t(vy1 + vy2)/2

▸ 虛擬程式碼轉為程式

ch15/cannonball.py
import math


def main():
    angle = float(input('Launch angle (in degrees): '))
    v = float(input('Initial velocity (in meters/second): '))
    py = float(input('Initial height (in meters): '))
    interval = float(input('Time interval (in seconds): '))

    # Convert angle to radians
    theta = math.radians(angle)
    vx = v * math.cos(theta)
    vy = v * math.sin(theta)

    print('\nThe trajectory:')
    print('    x       y')
    print('--------------')
        
    # Loop until the cannonball hits the ground
    px = 0.0
    while py >= 0.0:
        # Calculate position and velocity in interval seconds
        px += interval*vx
        vy2 = vy - interval*9.8
        py += interval*(vy+vy2)/2.0
        vy = vy2
        print(f'{px:>5.1f}\t{py:>5.1f}')    # Print the trajectory of the cannonball

    print(f'\nDistance traveled: {px:.1f} meters.')


if __name__ == '__main__':
    main()
float() 函式會將使用者輸入的數值字串轉為浮點數
print(...) 函式:
# :>5.1f 表示向右對齊,總共五個字元,一位小數
# \t 表示列印定位鍵 (固定寬度)
# :.1f 表示顯示一位小數
✶ 執行範例:
Launch angle (in degrees): 30
Initial velocity (in meters/second): 30
Initial height (in meters): 0
Time interval (in seconds): 0.1

The trajectory:
    x       y
-------------
  2.6     1.5
  5.2     2.8
  .
  .
 80.5    -0.6

Distance traveled: 80.5 meters.

∗ 第二版程式:模組化

▸ 由上往下設計:將程式分解為三個模組

cannonball2.png
✶ 輸入模組 getInputs():取得使用者的輸入
✶ 計算速度模組 getXYComponents():計算 xy 方向的速度分量
✶ 模擬砲彈路徑模組 simulate():以迴圈更新資料方式模擬砲彈路徑,最後回覆砲彈飛行距離

▸ 模組化程式

cannonball2.py
import math


def main():
    angle, v, py, interval = getInputs()
    vx, vy = getXYComponents(v, angle)
    px = simulate(interval, py, vx, vy)
    print(f'\nDistance traveled: {px:.1f} meters.')


def getInputs():
    angle = float(input('Launch angle (in degrees): '))
    v = float(input('Initial velocity (in meters/second): '))
    py = float(input('Initial height (in meters): '))
    interval = float(input('Time interval (in seconds): '))
    return angle, v, py, interval


def getXYComponents(v, angle):
    theta = math.radians(angle)
    vx = v * math.cos(theta)
    vy = v * math.sin(theta)
    return vx, vy


def simulate(interval, py, vx, vy):
    print('\nThe trajectory:')
    print('    x       y')
    print('--------------')
        
    px = 0.0
    while py >= 0.0:
        px += interval*vx
        vy2 = vy - interval*9.8
        py += interval*(vy+vy2)/2.0
        vy = vy2
        print(f'{px:>5.1f}\t{py:>5.1f}')    # Print the trajectory of the cannonball
    return px


if __name__ == '__main__':
    main()

✶ 此版本程式較為簡潔:
# 主程式變數的數量由原先的 10 個減少為 8 個 (thetavy 變成區域變數),將暫時的資料隱藏在區域中,有助於由上往下的設計模式
✶ 但程式還是有點複雜,特別是迴圈:
# 追蹤砲彈狀況需要 5 項資訊 (interval, px, py, vx, vy),其中 3 項 (px, py, vy) 每次都需要修改
# 變數數量太多,表示程式還有改善的空間

∗ 第三版程式:物件化

▸ 目前程式使用 4 個變數來描述砲彈 (px, py, vx, vy),這些變數分佈在各個模組裡, 讓程式看來很紛亂

✶ 假設規模稍大的問題,裡面有 20 個砲彈,那麼變數就需要使用串列 px[20], py[20], vx[20], vy[20], 再加上如果變數很多 (砲彈尺寸、重量、顏色、外觀、名稱 ...),我們可以想像得到,那將會是多少變數、多少串列元素在程式裡滿天飛!!

▸ 如果我們有一個砲彈 (Cannonball) 物件類別,所有描述砲彈的變數就是砲彈物件的屬性, 這些屬性是封在砲彈物件裡的,因此我們只會有一個串列變數 cannonball[20], 要讀取砲彈的某個變數只要用點號即可 (例如:cannonball[10].px), 程式乾淨許多

▸ 另外,如果我們好好設計這個砲彈類別,讓它「了解」物體的物理特性,也有模擬彈道的方法, 那麼主程式就只需要建立該物件並且呼叫模擬方法即可,問題結構變得單純許多:

import math


def main():
    angle, v, py, interval = getInputs()
    cannonball = Cannonball(angle, v, py)
    cannonball.simulate(interval)
    print(f'\nDistance traveled: {cannonball.px:.1f} meters.')
✶ 很顯然的,此版本更為簡潔,而且清楚說明演算法的涵義:
# 先收集必要資訊
# 產生一個砲彈物件
# 呼叫砲彈物件的模擬彈道方法
# 最後印出結果
✶ 因此只要定義適當 Cannonball 類別,然後實作 simulate() 方法即可,如下:
cannonball3.py
import math


class Cannonball:

    def __init__(self, angle, velocity, height):
        self.px = 0.0
        self.py = height
        theta = math.radians(angle)
        self.vx = velocity * math.cos(theta)
        self.vy = velocity * math.sin(theta)

    def simulate(self, interval):
        print('\nThe trajectory:')
        print('    x       y')
        print('--------------')
        
        while self.py >= 0:
            self.px += interval*self.vx
            vy2 = self.vy - 9.8*interval
            self.py += interval*(self.vy+vy2)/2.0
            self.vy = vy2
            print(f'{self.px:>5.1f}\t{self.py:>5.1f}')


def main():
    angle, v, h0, interval = getInputs()
    cannonball = Cannonball(angle, v, h0)
    cannonball.simulate(interval)
    print(f'\nDistance traveled: {cannonball.px:.1f} meters.')


def getInputs():
    angle = float(input('Launch angle (in degrees): '))
    v = float(input('Initial velocity (in meters/second): '))
    py = float(input('Initial height (in meters): '))
    interval = float(input('Time interval (in seconds): '))
    return angle, v, py, interval


if __name__ == '__main__':
    main()