上一篇討論了OOA/OOD,接著我們就來實作OOP,本篇先討論Python的類別(Class),並說明如何達成繼承(Inheritance)、封裝(Encapsulation)、多型(Polymorphism)、組合(Composition)...等設計理念。
範例1. 建立類別,包含屬性與方法,以銀行帳戶為例,程式名稱為bank_account.py。
class Account:
def __init__(self, id, name, balance=0):
self.id = id # 帳號
self.name = name # 姓名
self.balance = balance # 帳戶餘額
# 存款
def deposit(self, amount):
self.balance += amount
# 提款
def withdraw(self, amount):
self.balance -= amount
michael = Account('0001', 'michael', 100)
john = Account('0002', 'john', 0)
mary = Account('0003', 'mary')
michael.deposit(10_000)
michael.withdraw(3_000)
print(michael.balance)
執行結果:存款10,000,再提款3,000,加上初始餘額100,故10,000-3,000+100=7100。
測試self:修改deposit程式如下,重覆步驟3,得到【<class '__main__.Account'>】,驗證self的資料型別是類別Account。
def deposit(self, amount):
# 測試 self
print(type(self))
self.balance += amount
以Google雲端硬碟的draw.io功能繪製UML的類別圖如下:
Python v3.7 新增 dataclass decorator,可簡化類別寫法,不需要定義__init__。
範例2. 使用dataclass改寫bank_account.py,程式名稱為bank_account_with_dataclass.py。
from dataclasses import dataclass
@dataclass
class Account:
id:str # 帳號
name:str # 姓名
balance:int = 0 # 帳戶餘額
測試:與範例1結果相同。
顯示物件:dataclass自動實作__repr__方法,它定義【print物件】的內容。
print(michael)
Account(id='0001', name='michael', balance=7100)
print(michael == Account('0001', 'michael', 100))
print(michael == Account('0001', 'michael', 7100))
michael_2 = Account('0001', 'michael', 'x')
print(michael_2.balance)
執行結果:x。如要強制檢查輸入值是否正確,可參閱【Pydantic套件】。
如果不想指定特定資料型別,可使用Any,請參考any_test.py。
範例3. 在執行時期(Run time)動態產生類別,程式名稱為make_dataclass_test.py。
from dataclasses import make_dataclass
Account = make_dataclass('Account', ['id', 'name', 'balance'])
# 測試物件創建
michael = Account('0001', 'michael', 100)
# 顯示物件
print(michael)
print(michael == Account('0001', 'michael', 100))
@dataclass可指定參數:
範例4. 比較物件大小順序,以樸克牌為例,程式修改自【Data Classes in Python 3.7+ (Guide)】,程式名稱為order_dataclass.py。
from dataclasses import dataclass, field
# 指定樸克牌大小順序
RANKS = '2 3 4 5 6 7 8 9 10 J Q K A'.split()
SUITS = '♣ ♢ ♡ ♠'.split()
@dataclass(order=True)
class PlayingCard:
sort_index: int = field(init=False, repr=False)
rank: str
suit: str
# 計算排序索引值
def __post_init__(self):
self.sort_index = (RANKS.index(self.rank) * len(SUITS)
+ SUITS.index(self.suit))
# 測試物件創建
queen_of_hearts = PlayingCard('Q', '♡♠'[0]) # 單獨顯示紅心會出現亂碼
ace_of_spades = PlayingCard('A', '♠')
# 比較物件大小順序
print(ace_of_spades > queen_of_hearts)
範例5. 改寫範例2,程式名稱為inheritance_test.py。
@dataclass
class Wage_account(Account): # 繼承Account
company_id:str # 公司代碼
employee_id:str # 員工代碼
# 測試物件創建
michael = Wage_account('0001', 'michael', 100, 'TSMC', '2020010001')
michael_2 = Wage_account(id='0001', name='michael', balance=100,
company_id='TSMC', employee_id='2020010001')
# 顯示物件
print(michael)
print(michael_2)
Wage_account(id='0001', name='michael', balance=100, company_id='TSMC', employee_id='2020010001')
Wage_account(id='0001', name='michael', balance=100, company_id='TSMC', employee_id='2020010001')
繪製UML的類別圖如下:
類別之間的關係不只有繼承,也可以使用組合(Composition),例如一輛轎車有4個輪胎,有許多人認為組合在重用性(Reusibility)與彈性(Flexibility)較佳,因為組合是【鬆散耦合】(Loosely coupled),繼承的父類別規格修改,子類別的屬性與方法隨之變動,會造成系統必須全部重新審視,茲事體大,相關內容可參閱類別之間的關係不只有繼承,也可以使用組合(Composition),例如一輛轎車有4個輪胎,有許多人認為組合在重用性(Reusibility)與彈性(Flexibility)較佳,因為組合是【鬆散耦合】(Loosely coupled),繼承的父類別規格修改,子類別的屬性與方法隨之變動,會造成系統必須全部重新審視,茲事體大,相關內容可參閱【Composition vs Inheritance】。
範例6. 以一輛轎車有4個輪胎為例,使用組合建立類別,程式名稱為composition_test.py。
from dataclasses import dataclass
from typing import List
from enum import Enum, auto
class Position(Enum):
FRONT_LEFT = auto()
FRONT_RIGHT = auto()
BACK_LEFT = auto()
BACK_RIGHT = auto()
# 輪胎
@dataclass
class Wheel:
position:Position # 安裝位置
brand:str # 廠牌
# 轎車
@dataclass
class Car(): # 轎車
type:str # 轎車類別
power:int # 馬力
size:int # 輪胎尺寸
wheels: List[Wheel] # 輪胎
# 測試物件創建
car1 = Car(type='type_a', power=2000, size=215, tires=[
Tire(position=Position.FRONT_LEFT, brand='米其林'),
Tire(position=Position.FRONT_RIGHT, brand='普利司通'),
Tire(position=Position.BACK_LEFT, brand='米其林'),
Tire(position=Position.BACK_RIGHT, brand='普利司通')
])
# 顯示物件
print(car1)
Car(type='type_a', power=2000, size=215, tires=[Tire(position=<Position.FRONT_LEFT: 1>, brand='米其林'), Tire(position=<Position.FRONT_RIGHT: 2>, brand='普利司通'), Tire(position=<Position.BACK_LEFT: 3>, brand='米其林'), Tire(position=<Position.BACK_RIGHT: 4>, brand='普利司通')])
繪製UML的類別圖如下:
Python可以封裝某些屬性,讓外界必須透過方法(Method)才能修改屬性。
範例7. 以轎車類別為例,程式名稱為encapsulation_test.py。
from dataclasses import dataclass, field
# 轎車
@dataclass
class Car(): # 轎車
type:str # 轎車類別
power:int # 馬力
size:int # 輪胎尺寸
_year:int = field(init=False, repr=False) # 出廠年度
def set_year(self, year):
self._year = year
# 測試物件創建
car1 = Car('type_a', 2000, 215)
car1.set_year(2020)
# 顯示物件
print(car1)
print(car1._year > 2022)
Car(type='type_a', power=2000, size=215)
False
Python也支援多型(Polymorphism),可以讓多個類型的物件呼叫同名的方法,如果子類別未定義該方法,會自動呼叫父類別的方法。
範例7. 定義父類別為【載具】(Vehicle),轎車(Car)、小艇(Boat)及飛機(Plane)3個子類別繼承Vehicle,程式名稱為polymorphism_test.py,修改自【Python Polymorphism, w3schools】。
# 載具
class Vehicle:
def __init__(self, brand, model):
self.brand = brand
self.model = model
# 載具方法
def move(self):
print("Move!")
# 轎車
class Car(Vehicle):
pass
# 小艇
class Boat(Vehicle):
def move(self):
print("Sail!")
# 飛機
class Plane(Vehicle):
def move(self):
print("Fly!")
# 物件創建
car1 = Car("Ford", "Mustang")
boat1 = Boat("Ibiza", "Touring 20")
plane1 = Plane("Boeing", "747")
# 執行物件方法 move
for x in (car1, boat1, plane1):
print(x)
x.move()
<__main__.Car object at 0x0000013002598490>
Move!
<__main__.Boat object at 0x00000130025980D0>
Sail!
<__main__.Plane object at 0x0000013002571160>
Fly!
本篇我們使用Python實作OOP,並介紹新的語法dataclass,同時實作繼承(Inheritance)、封裝(Encapsulation)、多型(Polymorphism)、組合(Composition)...等設計理念,下次我們就來開發【打磚塊】(Breakout)遊戲。
本系列的程式碼會統一放在GitHub,本篇的程式放在src/11資料夾,歡迎讀者下載測試,如有錯誤或疏漏,請不吝指正。