類別入門

April 23, 2022

你也許會想設計一個銀行商務相關的簡單程式,具有建立帳戶、存款、提款等功能:

def account(name, number, balance):
    return {'name': name, 'number': number, 'balance': balance}

def deposit(acct, amount):
    if amount <= 0:
        print('存款金額不得為負')
    else:
        acct['balance'] += amount

def withdraw(acct, amount):
    if amount > acct['balance']:
        print('餘額不足')
    else:
        acct['balance'] -= amount

def desc(acct):
    return f'Account:{acct}'

acct = account('Justin', '123-4567', 1000)
deposit(acct, 500)
withdraw(acct, 200)

# 顯示 Account:{'balance': 1300, 'number': '123-4567', 'name': 'Justin'}
print(desc(acct))   

這些函式操作,都是與傳入的 dict 實例,也就是代表帳戶狀態的物件高度相關,何不將它們組織在一起呢?

自訂型態

可以為帳戶建立一個專屬型態,擁有專用屬性,然後讓存款、提款等函式,專屬於這個帳戶型態的實例,這樣在設定物件狀態、思考物件可用的操作時,都會比較方便一些,來修改以上的程式碼,看看是否真的能增加可用性(Usability)。

在 Python 可以使用 class 來建立一個專屬型態:

class Account:
    pass

def account(name, number, balance):
    acct = Account()
    acct.name = name
    acct.number = number
    acct.balance = balance
    return acct

def deposit(acct, amount):
    if amount <= 0:
        print('存款金額不得為負')
    else:
        acct.balance += amount

def withdraw(acct, amount):
    if amount > acct.balance:
        print('餘額不足')
    else:
        acct.balance -= amount

def desc(acct):
    return return f"Account('{acct.name}', '{acct.number}', {acct.balance})"   

在這個範例中定義了 Account 類別,類別是物件的藍圖,目前還沒有在藍圖中加上任何定義,只是單純地 pass,目的只是先讓帳戶有個專屬型態 Account

想要建立 Account 實例,可以呼叫 Account()(這相當於依照藍圖來製作出一個成品),接著在實例上設置相關屬性,這麼一來,型態與屬性就有了特定關聯,也就是看到 Account,就想到有 namenumberbalance 等,進一步地,可在 Account 上使用點運算子 . 來存取這些屬性,這會比原先使用 dict 實例來得好,畢竟鍵值存取操作,才是專屬於 dict 這個型態。

類別的實例建立之後,還可以動態地增減其屬性的特性,常被稱為物件個體化(Object individuation)。

因為 accountdepositwithdrawdesc 函式的外觀並沒有改變,只是更改了內部實作,因此完成修改後,可以直接執行程式。

雖然已經定義了 Account 類別,作為帳戶的專屬型態,然而 accountdepositwithdrawdesc 函式卻定義在其他地方,明明它們都是與 Account 實例相關的操作,將相關的操作放在一起而不是分開,是設計時的一個基本原則,對物件導向更是如此。

__init__、__str__、__repr__ 方法

來看看 account 函式,它定義了如何建立實例,以及實例建立後的相關屬性設定,這是每個 Account 實例都要經歷的初始化流程,可以將初始化流程,使用 __init__ 方法定義在類別之中。例如:

class Account:
    def __init__(self, name, number, balance):
        self.name = name
        self.number = number
        self.balance = balance

可以看到方法前後各有兩個相連底線,在 Python 中,這樣的名稱意謂著,在類別以外的其他位置,不要直接呼叫,基本上都會有個函式或類別可用來呼叫這類方法,就 __init__ 而言,若如下建立 Account 實例時就會呼叫:

acct = Account('Justin', '123-4567', 1000)

在呼叫 __init__ 方法時,建立的 Account 實例會傳入作為方法的第一個參數,雖然第一個參數的名稱可以自訂,然而在 Python 的慣例中,第一個參數的名稱會命名為 self(其他語言中通常是以關鍵字 this 存在)。

在建立類別實例時,若有其他的參數,可以從第二個參數開始依序定義,若要設定實例屬性,可以透過 self 與點運算子來設置,__init__ 方法不傳回任何值,acct = Account('Justin', '123-4567', 1000) 執行過後,就會將建立的實例指定給 acct

在 Python 中有個特殊名稱 __str__,專門用來定義傳回物件描述字串的方法,可用來取代方才的 desc 函式:

class Account:
    
    def __str__(self):
        return f"Account('{self.name}', '{self.number}', {self.balance})"

同樣地,方法的第一個參數定義為 self,用來接受物件本身,正如先前所述,方法前後各有兩個連線底線,在 Python 意謂著,不要直接去呼叫,基本上會有函式或類別可用來呼叫這類方法,就 __str__ 而言,print 函式是個例子,在執行 print(acct) 時,就會呼叫 acct__str__ 方法取得描述字串,然後顯示描述字串。

另一個例子是 str,若執行 str(acct) 時,就會呼叫 acct__str__ 方法取得描述字串並傳回。

實際上,類別也可以定義 __repr__ 方法,當執行 repr(acct) 時,就會呼叫 acct__repr__ 方法取得描述字串並傳回。

__str__ 字串描述主要是給人類看的易懂格式,而 __repr__ 通常會傳回給程式、機器剖析用的特定格式(像是對代表日期的字串剖析,以建立一個日期物件),或者是包含除錯用的字串資訊,Python 內建型態的 __repr__ 傳回的字串,會是個有效的 Python 運算式(expression),可以使用 eval 運算來產生一個內含值相同的物件。

操作方法

接著將 deposit 以及 withdraw 也定義在 Account 類別之中。例如:

class Account:
    
    def deposit(self, amount):
        if amount <= 0:
            print('存款金額不得為負')
        else:
            self.balance += amount

    def withdraw(self, amount):
        if amount > self.balance:
            print('餘額不足')
        else:
            self.balance -= amount

depositwithdraw 移至 Account 類別後的主要修改,在於第一個參數名稱,在 Python 中,物件方法第一個參數一定是物件本身,就 Python 的設計哲學(Zen of Python)來說,這是「Explicit is better than implicit」的實踐,因為在方法的實作中,以 self 作為前置的名稱,就代表著存取實例本身的屬性,而不是存取區域變數。

定義在 Account__init__depositwithdraw,本質上也是函式,不過在物件導向的術語中,對這些定義在類別,可對物件進行的操作,習慣上會稱為方法(Method)。

來看看修改後的程式最後長什麼樣子:

class Account:
    def __init__(self, name, number, balance):
        self.name = name
        self.number = number
        self.balance = balance

    def deposit(self, amount):
        if amount <= 0:
            print('存款金額不得為負')
        else:
            self.balance += amount

    def withdraw(self, amount):
        if amount > self.balance:
            print('餘額不足')
        else:
            self.balance -= amount

    def __str__(self):
        return f"Account('{self.name}', '{self.number}', {self.balance})"  


acct = Account('Justin', '123-4567', 1000)
acct.deposit(500)
acct.withdraw(200)

# 顯示 Account('Justin', '123-4567', 1300)
print(acct)   

分享到 LinkedIn 分享到 Facebook 分享到 Twitter