類別入門
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
,就想到有 name
、number
、balance
等,進一步地,可在 Account
上使用點運算子 .
來存取這些屬性,這會比原先使用 dict
實例來得好,畢竟鍵值存取操作,才是專屬於 dict
這個型態。
類別的實例建立之後,還可以動態地增減其屬性的特性,常被稱為物件個體化(Object individuation)。
因為 account
、deposit
、withdraw
、desc
函式的外觀並沒有改變,只是更改了內部實作,因此完成修改後,可以直接執行程式。
雖然已經定義了 Account
類別,作為帳戶的專屬型態,然而 account
、deposit
、withdraw
、desc
函式卻定義在其他地方,明明它們都是與 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
將 deposit
、withdraw
移至 Account
類別後的主要修改,在於第一個參數名稱,在 Python 中,物件方法第一個參數一定是物件本身,就 Python 的設計哲學(Zen of Python)來說,這是「Explicit is better than implicit」的實踐,因為在方法的實作中,以 self
作為前置的名稱,就代表著存取實例本身的屬性,而不是存取區域變數。
定義在 Account
的 __init__
、deposit
、withdraw
,本質上也是函式,不過在物件導向的術語中,對這些定義在類別,可對物件進行的操作,習慣上會稱為方法(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)