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) 及半徑 (radius),預設值都是 0
# 定義一個方法 setColor():用來設定顏色,有兩個參數 selfcolor
* 類別方法的第一個參數一定是 self (亦可改用其他名稱,但習慣上都是使用 self為什麼一定要有這個參數?)
* 在方法中存取物件屬性需在屬性名稱前加上 self.,此例中只要執行 setColor() 方法,就會動態新增一個 color 屬性
* 在方法中沒有 self. 前置的變數是區域變數 (方法執行完畢即不存在),例如以下紅色 colorarea 都是區域變數:
class Circle:
    def setColor(self, color):
        self.color = color
         area = math.pi * self.radius**2

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

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

(3) 產生物件

∗ 物件的產生

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

<variableName> = <className>()

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

▸ 以 Circle 類別為例,以下產生一個名為 circle 的實例:

circle = Circle()

∗ 存取屬性與執行方法

▸ 語法:都是利用點號

<object>.<attributeName>     # 存取屬性
<object>.<methodName>()      # 執行方法

▸ 例如:

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

∗ 物件的初始設定

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

class <className>:

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

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

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

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

ch15/circle.py
class Circle:
    cx = 0
    cy = 0
    radius = 0

    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 的圓
✶ 印出屬性值:
class Circle:
    ...

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

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

(4) 範例 1:大學課程

∗ 課程類別

▸ 學生所修習的課程,包含下列資訊:

✶ 該課程的學分數 (Credits)
✶ 成績之等級 (Grade) 及點數 (Point):A (4 點)、B (3 點)、C (2 點)、D (1 點)、及F (0 點) 共 5 個等級
✶ 平均點數 (Grade point average, GPA):將各課程所得之點數乘以學分數,加總後再除以總學分數

▸ 假設 ch15/students.txt 檔案紀錄學生的成績,每一行包含學生姓名、總學分數、及總點數,例如:

張三 127 228
李四 100 400
王五 18 41.5
趙六 48.5 155
劉七 37 125.33

∗ 程式需求:找出 GPA 最高的學生

▸ 首先建立學生類別

ch15/student.py
class Student:
    def __init__(self, name, credits, points):
        self.name = name
        self.credits = float(credits)
        self.points = float(points)
✶ 在 __init__() 方法裡設定三項屬性:姓名、學分數、點數
✶ 使用 float()creditspoints 的型態彈性更大,可以是整數、實數、甚至字串

▸ 然後,決定物件應該需要哪些方法,很顯然的,需要計算 GPA:

class Student:
    def __init__(self, name, credits, points):
        ...

    def gpa(self):
        return self.points/self.credits

▸ 類別建立完成後,擬定解決問題的虛擬程式碼:

Ask the user to enter the input file name
Open the file for reading
Set best to be the first student
For each of the rest students s in the file
    if s.gpa() > best.gpa
        set best to s
Print out information about best

▸ 將虛擬程式碼轉為程式

class Student:
    def __init__(self, name, credits, points):
        self.name = name
        self.credits = float(credits)
        self.points = float(points)

    def gpa(self):
        return self.points/self.credits


def makeStudent(line):
    # line  contains name, credits, and points
    # returns a corresponding Student object
    name, credits, points = line.split()
    return Student(name, credits, points)


def main():
    # Open the input file for reading
    filename = input('Enter the name of the grade file: ')
    with open(filename, 'r') as infile:
        # Set best to the record for the first student in the file
        best = makeStudent(infile.readline())

        # Process subsequent lines of the file
        for line in infile:
            # Turn the line into a student record
            s = makeStudent(line)
            # If this student is best so far, remember it
            if s.gpa() > best.gpa():
                best = s

    # Print information about the best student
    print('The best student is: ', best.name)
    print('Credits: ', best.credits)
    print('GPA: ', best.gpa())


if __name__ == '__main__':
    main()
✶ 其中 makeStudent() 讀取檔案一行,利用空白分解出 3 項資料,然後回覆學生物件
✶ 在迴圈開始之前設定 best = makeStudent(infile.readline()),之後再利用迴圈比較得到最高 GPA 的學生
✶ 結果:
Enter the name of the grade file: students.txt
The best student is: 李四
Credits: 100.0
GPA: 4.0

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

∗ 程式開發流程:

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 = eval(input('Launch angle (in degrees): '))
    v = eval(input('Initial velocity (in meters/second): '))
    py = eval(input('Initial height (in meters): '))
    interval = eval(input('The interval: '))

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

    # Loop until the cannonball hits the ground
    print('\nThe trajectory:')
    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()
eval() 函式會將使用者輸入的數值字串轉為數值
print(...) 函式:
# :>5.1f 表示向右對齊,總共五個字元,一位小數
# \t 表示列印定位鍵 (固定寬度)
# :.1f 表示顯示一位小數
✶ 執行範例:
Launch angle (in degrees): 30
Initial velocity (in meters/second): 30
Initial height (in meters): 0
The interval: 0.1

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

Distance traveled: 80.5 meters.

∗ 第二版程式 cannonball2.py:模組化

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

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

▸ 模組化程式

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 = eval(input('Launch angle (in degrees): '))
    v = eval(input('Initial velocity (in meters/second): '))
    py = eval(input('Initial height (in meters): '))
    interval = eval(input('Interval between position calculations: '))
    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:')
    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) 每次都需要修改
# 變數數量太多,表示程式還有改善的空間

∗ 第三版程式 cannonball3.py:物件化

▸ 目前程式使用 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):
        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 = eval(input('Launch angle (in degrees): '))
    v = eval(input('Initial velocity (in meters/second): '))
    py = eval(input('Initial height (in meters): '))
    interval = eval(input('Interval between position calculations: '))
    return angle, v, py, interval


if __name__ == '__main__':
    main()

上一章