iT邦幫忙

2024 iThome 鐵人賽

DAY 11
0
Python

Python 錦囊密技系列 第 11

【Python錦囊㊙️技11】OOP 實作(1) -- 入門

  • 分享至 

  • xImage
  •  

前言

上一篇討論了OOA/OOD,接著我們就來實作OOP,本篇先討論Python的類別(Class),並說明如何達成繼承(Inheritance)、封裝(Encapsulation)、多型(Polymorphism)、組合(Composition)...等設計理念。

類別(Class)

範例1. 建立類別,包含屬性與方法,以銀行帳戶為例,程式名稱為bank_account.py。

  1. 建立類別:屬性有帳號、姓名及帳戶餘額,方法含存款、提款。
  • 通常命名原則為【屬性使用名詞,方法使用動詞】
  • 類別首個字母為大寫(Capitalize)。
  • __init__:物件初始化函數,物件建立時會自動呼叫此函數,通常會同時指定所有屬性的初始值。
  • 方法第一個參數為self,呼叫方法時不需輸入,系統會自動填入物件(Object)本身。物件是類別的實體化(instance),可以將類別看作一種資料型別,物件是變數。
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
  1. 測試物件創建:建立3個物件。
michael = Account('0001', 'michael', 100)
john = Account('0002', 'john', 0)
mary = Account('0003', 'mary')
  1. 呼叫類別的方法:存款10,000,再提款3,000。
michael.deposit(10_000)
michael.withdraw(3_000)
print(michael.balance)
  1. 執行結果:存款10,000,再提款3,000,加上初始餘額100,故10,000-3,000+100=7100。

  2. 測試self:修改deposit程式如下,重覆步驟3,得到【<class '__main__.Account'>】,驗證self的資料型別是類別Account。

def deposit(self, amount):
    # 測試 self
    print(type(self))
    self.balance += amount

以Google雲端硬碟的draw.io功能繪製UML的類別圖如下:
https://ithelp.ithome.com.tw/upload/images/20240924/20001976G2m9Uetgma.png

data class

Python v3.7 新增 dataclass decorator,可簡化類別寫法,不需要定義__init__。
範例2. 使用dataclass改寫bank_account.py,程式名稱為bank_account_with_dataclass.py。

  1. 建立類別:方法不變,屬性直接定義,不需__init__,屬性必須指定資料型別,但資料型別不具強制性,只具有暗示性,故稱為【Type hint】。
from dataclasses import dataclass

@dataclass
class Account:
    id:str      # 帳號
    name:str    # 姓名
    balance:int = 0 # 帳戶餘額
  1. 測試:與範例1結果相同。

  2. 顯示物件:dataclass自動實作__repr__方法,它定義【print物件】的內容。

print(michael)
  1. 執行結果:
Account(id='0001', name='michael', balance=7100)
  1. dataclass也會自動實作__eq__方法,它定義物件的【比較】(==)。測試如下:
print(michael == Account('0001', 'michael', 100))
print(michael == Account('0001', 'michael', 7100))
  1. 執行結果:
  • 第一列:False,michael balance=7100,與==右邊balance(100)不等。
  • 第一列:True,因為michael物件所屬類別與屬性均相等。
  1. 【Type hint】測試如下:balance定義為int,但物件創建時指定balance為字串('x'),並不會出現錯誤。
michael_2 = Account('0001', 'michael', 'x')
print(michael_2.balance)
  1. 執行結果:x。如要強制檢查輸入值是否正確,可參閱【Pydantic套件】

  2. 如果不想指定特定資料型別,可使用Any,請參考any_test.py。

範例3. 在執行時期(Run time)動態產生類別,程式名稱為make_dataclass_test.py。

  1. 動態建立類別:不需指定資料型別。
from dataclasses import make_dataclass

Account = make_dataclass('Account', ['id', 'name', 'balance'])
  1. 測試:執行結果與範例2結果相同。
    # 測試物件創建
    michael = Account('0001', 'michael', 100)
    # 顯示物件
    print(michael)
    print(michael == Account('0001', 'michael', 100))

@dataclass 參數

@dataclass可指定參數:

  1. order:可以比較物件大小順序。
  2. frozen:物件一旦建立後,即不可修改,稱之為Immutable。

範例4. 比較物件大小順序,以樸克牌為例,程式修改自【Data Classes in Python 3.7+ (Guide)】,程式名稱為order_dataclass.py。

  1. 建立類別:@dataclass可指定參數order=True,定義sort_index屬性,並在__post_init__方法計算sort_index(排序索引值)。
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)) 
  1. 測試:物件比較預設會以第一個屬性比較,故執行結果Q<A。
    # 測試物件創建
    queen_of_hearts = PlayingCard('Q', '♡♠'[0]) # 單獨顯示紅心會出現亂碼
    ace_of_spades = PlayingCard('A', '♠')
    
    # 比較物件大小順序
    print(ace_of_spades > queen_of_hearts)

繼承(Inheritance)

範例5. 改寫範例2,程式名稱為inheritance_test.py。

  1. 建立子類別【薪轉帳戶】(Wage_account)繼承Account,額外增加兩個屬性。
@dataclass
class Wage_account(Account): # 繼承Account
    company_id:str  # 公司代碼
    employee_id:str # 員工代碼
  1. 測試:子類別屬性會跟在父類別屬性後面,也可使用命名參數指定屬性值。
# 測試物件創建
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)
  1. 執行結果:
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的類別圖如下:
https://ithelp.ithome.com.tw/upload/images/20240924/20001976B6l9A4NfPW.png

組合(Composition)

類別之間的關係不只有繼承,也可以使用組合(Composition),例如一輛轎車有4個輪胎,有許多人認為組合在重用性(Reusibility)與彈性(Flexibility)較佳,因為組合是【鬆散耦合】(Loosely coupled),繼承的父類別規格修改,子類別的屬性與方法隨之變動,會造成系統必須全部重新審視,茲事體大,相關內容可參閱類別之間的關係不只有繼承,也可以使用組合(Composition),例如一輛轎車有4個輪胎,有許多人認為組合在重用性(Reusibility)與彈性(Flexibility)較佳,因為組合是【鬆散耦合】(Loosely coupled),繼承的父類別規格修改,子類別的屬性與方法隨之變動,會造成系統必須全部重新審視,茲事體大,相關內容可參閱【Composition vs Inheritance】

範例6. 以一輛轎車有4個輪胎為例,使用組合建立類別,程式名稱為composition_test.py。

  1. 先建立輪胎類別,屬性【安裝位置】(position)採用enum,再建立轎車類別,含輪胎list屬性。
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] # 輪胎
  1. 測試:建立轎車物件,含4個輪胎。
# 測試物件創建
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)
  1. 執行結果:
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的類別圖如下:
https://ithelp.ithome.com.tw/upload/images/20240924/20001976qiGXIbeaoP.png

封裝(Encapsulation)

Python可以封裝某些屬性,讓外界必須透過方法(Method)才能修改屬性。
範例7. 以轎車類別為例,程式名稱為encapsulation_test.py。

  1. 先建立輪胎類別,屬性【_year】採用【_】開頭命名,表屬性為私有(Private)的,但並沒有強制性,需透過field參數設定,init=False表物件初始化不可指定【_year】屬性,並設定repr=False,使列印(print)物件時,不會顯示【_year】屬性。
from dataclasses import dataclass, field

# 轎車
@dataclass
class Car(): # 轎車
    type:str  # 轎車類別
    power:int # 馬力
    size:int  # 輪胎尺寸
    _year:int = field(init=False, repr=False) # 出廠年度
  1. 【_year】屬性必須透過方法才能修改。
    def set_year(self, year):
        self._year = year
  1. 測試:建立轎車類別,不可指派【_year】屬性值,但可以讀取。
# 測試物件創建
car1 = Car('type_a', 2000, 215)
car1.set_year(2020)

# 顯示物件
print(car1)
print(car1._year > 2022)
  1. 執行結果:雖然print物件時看不到【_year】屬性,但可以讀取,與2022比較。
Car(type='type_a', power=2000, size=215)
False

多型(Polymorphism)

Python也支援多型(Polymorphism),可以讓多個類型的物件呼叫同名的方法,如果子類別未定義該方法,會自動呼叫父類別的方法。
範例7. 定義父類別為【載具】(Vehicle),轎車(Car)、小艇(Boat)及飛機(Plane)3個子類別繼承Vehicle,程式名稱為polymorphism_test.py,修改自【Python Polymorphism, w3schools】

  1. 先建立類別:Car、Boat及Plane 3個類別繼承Vehicle,並定義move方法。
# 載具
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!")
  1. 測試:創建轎車(Car)、小艇(Boat)及飛機(Plane)3個物件,並執行每個子類別的方法【move】。
# 物件創建
car1 = Car("Ford", "Mustang")  
boat1 = Boat("Ibiza", "Touring 20") 
plane1 = Plane("Boeing", "747") 

# 執行物件方法 move
for x in (car1, boat1, plane1):
  print(x)
  x.move()
  1. 執行結果:可以使用迴圈呼叫每個子類別的方法【move】,Car未定義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資料夾,歡迎讀者下載測試,如有錯誤或疏漏,請不吝指正。


上一篇
【Python錦囊㊙️技10】OOA、OOD and OOP
下一篇
【Python錦囊㊙️技12】OOP 實作(2) -- 遊戲開發
系列文
Python 錦囊密技30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言