屬性與方法
April 24, 2022在〈類別入門〉中,Account
類別擁有 name
、number
與 balance
三個屬性可供存取,雖然你設計了deposit
、withdraw
方法,希望使用者想變更 Account
物件的狀態時,都透過這些方法,然而,可能會有人如下誤用:
acct = Account('Justin', '123-4567', 1000)
acct.balance = 1000000
定義內部屬性
如果想避免使用者這類誤用,可以使用 self.__xxx
的方式定義內部值域。例如:
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})"
在 Account
類別的方法定義中,可以使用 self.__name
、self.__number
、self.__balance
來存取屬性;然而,若使用者建立 Account
實例並指定給 acct
,不能使用 acct.__name
、acct.__number
、acct.__balance
來進行屬性的存取(子類別中定義的方法也不能以 self.__name
、self.__number
、self.__balance
來存取屬性),這會引發 AttributeError
,因為屬性若使用 __xxx
這樣的名稱,會自動轉換為「_類別名稱__xxx」。
Python 沒有完全阻止存取,只要在原本的屬性名稱前加上 _類別名稱
,仍舊可以存取到名稱為 __
開頭的屬性:
acct = Account('Justin', '123-4567', 1000)
print(acct._Account__name)
acct._Account__balance = 1
然而並不建議這麼做,__xxx
名稱的屬性,慣例上是作為類別定義時,內部相關流程操作之用,外界最好不要知道其存在,更別說是操作了,如果真想這麼做,最好是清楚地知道自己在做些什麼。
兩個底線 __
開頭的屬性,基本上表示私有(private),只在定義的類別中使用;如果屬性想讓子類別存取,Python 的慣例會使用一個底線 _
,代表它是一個受保護的(protected)屬性,這僅僅只是一個提示,python
直譯器不會做任何變換,只是提示使用者存取該屬性必須慎重。
定義外部屬性
之前使用了 __xxx
這樣的格式來定義了內部屬性,不過留下了一個問題,若只是想取得帳戶名稱、帳號、餘額等資訊,以便在相關使用者介面上顯示,那該怎麼辦呢?以 acct._Account__name
這樣的方式是不建議的,那還能用什麼方式?
基本上,可以直接定義一些方法來傳回 self.__name
、self.__number
這些內部屬性的值,例如:
class Account:
一些程式碼…略
def name(self):
return self.__name
def number(self):
return self.__number
def balance(self):
return self.__balance
這麼一來,使用者就可以使用 acct.name()
、acct.number()
這樣的方式來取得值,不過,針對這種情況,可以考慮在這類方法上加註 @property
,例如:
class Account:
def __init__(self, name, number, balance):
self.__name = name
self.__number = number
self.__balance = balance
@property
def name(self):
return self.__name
@property
def number(self):
return self.__number
@property
def balance(self):
return self.__balance
其他程式碼同前一範例,故略…
acct = Account('Justin', '123-4567', 1000)
acct.deposit(500)
acct.withdraw(200)
print('帳戶名稱:', acct.name)
print('帳戶號碼:', acct.number)
print('帳戶餘額:', acct.balance)
現在使用者可以使用 acct.name
、acct.number
、acct.balance
這樣的形式取得值,然而,就目前的程式碼撰寫,無法直接使用 acct.balance = 10000
這樣的形式來設定屬性值,因為 @property
只允許 acct.balance
這樣的形式取值。
如果在程式設計的一開始,沒有使用 self.__balance
的方式,而是以 self.balance
定義內部屬性,而使用者也使用了 acct.balance
取得值,後來進一步考慮要避免被誤用,想修改為 self.__balance
定義內部屬性,這時就可以像上面的範例,定義一個方法並加註 @property
,如此一來,使用者原本的程式碼也不會受到影響,這是固定存取原則(Uniform access principle)的實現。
如果這個範例,想進一步提供 acct.balance = 10000
這樣的形式,可以使用 @name.setter
、@number.setter
、@balance.setter
標註對應的方法。例如:
class Account:
略…
@name.setter
def name(self, name):
# 可實作一些設值時的條件控制
self.__name = name
@number.setter
def number(self, number):
# 可實作一些設值時的條件控制
self.__number = number
@balance.setter
def balance(self, balance):
# 可實作一些設值時的條件控制
self.__balance = balance
略…
被 @property
標註的 xxx
取值方法(Getter),可以使用 @xxx.setter
標註對應的設值方法(Setter),使用 @xxx.deleter
來標註對應的刪除值之方法。取值方法傳回值可以是即時運算的結果,而設值方法中,必要時可以使用流程語法等來實作一些存取控制。
綁定與未綁定方法
定義在類別中的方法,本質上也是函式,以目前定義的 Account
類別為例,執行 type(Account.deposit)
的話,會得到 <class 'function'>
,如果建立了一個實例並指定給 acct
,呼叫 acct.deposit(500)
時,會將 acct
參考的實例傳給 deposit
的第一個 self
參數。實際上,也可以如下取得相同效果:
acct = Account('Justin', '123-4567', 1000)
Account.deposit(acct, 500)
不過,若試著將 acct.deposit
或 acct.withdraw
指定給一個變數,會發現變數實際上參考著一個綁定方法:
>>> acct = Account('Justin', '123-4567', 1000)
>>> deposit = acct.deposit
>>> withdraw = acct.withdraw
>>> deposit
<bound method Account.deposit of <bank.Account object at 0x00000212ECE54970>>
>>> withdraw
<bound method Account.withdraw of <bank.Account object at 0x00000212ECE54970>>
>>> deposit(500)
>>> withdraw(200)
>>> print(acct)
Account(Justin, 123-4567, 1300)
>>>
綁定方法是 method
的實例(type(acct.deposit)
會得到 <class 'method'>
),行為上可以像函式進行呼叫,試著呈現 acct.deposit
或 acct.withdraw
的字串描述時,會出現 'bound method'
這樣的字樣,也就是說,此綁定方法綁定了一個 Account
實例,也就是方法的第一個參數 self
參考至 Account
實例。
在 Python 中,實作了 __call__
方法的物件,行為上可以像函式進行呼叫,稱為 callable 物件,其實到目前為止,你已經用過不少 callable 物件,例如,類別本身就是個 callable 物件。
使用 acct.deposit(500)
的方式來呼叫方法時,acct
參考的物件,實際上就會傳給 deposit
方法的第一個參數,相對地,如果在類別中定義了一個方法,沒有任何參數會怎樣呢?
>>> class Some:
... def nothing():
... print('nothing')
...
>>> s = Some()
>>> s.nothing()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: nothing() takes 0 positional arguments but 1 was given
>>>
如果透過類別的實例呼叫方法時,點運算子左邊的物件會傳給方法作為第一個引數,然而,這邊的 nothing
方法沒有定義任何參數,因此發生了 TypeError
,並說明錯誤在於,試圖在呼叫時給予一個引數。
有沒有辦法取得綁定方法綁定的物件呢?雖然不鼓勵,不過確實可以透過綁定方法的 __self__
屬性來取得。
相對於綁定方法,像這樣定義在類別中,沒有定義 self
參數的方法,稱為未綁定方法(Unbound method),這類方法,充其量只是將類別名稱作為一種名稱空間,可以透過類別名稱來呼叫它,或取得函式物件進行呼叫:
>>> Some.nothing()
nothing
>>> nothing = Some.nothing
>>> nothing()
nothing
>>>
靜態方法
現在假設,想在 Account
類別增加一個 default
函式,以便建立預設帳戶,只需要指定名稱與帳號,開戶時餘額預設為 100:
class Account:
…略
def default(name, number):
return Account(name, number, 100)
當然,這個需求也可以在 __init__
上使用預設引數來達成,這邊只是為了示範,在更真實的情境中,可能是建構實例前,有個複雜的流程,像是收集、確認開戶者的其他資格等,因而有了 default
這類函式存在的需求。
你原本的用意,是希望 default
函式是以 Account
類別作為名稱空間,因為它與建立帳戶有關,而使用者應該要以 Account.default('Monica', '765-4321')
這樣的方式來呼叫它,然而,若使用者如下誤用,正好也能夠執行:
acct = Account('Justin', '123-4567', 1000)
# 顯示Account(Account(Justin, 123-4567, 1000), 1000, 100)
print(acct.default(1000))
就這個例子來說,acct
參考的物件,傳給了 default
方法的第一個參數name,而執行過程正好也沒有引發錯誤,只不過顯示了怪異的結果。
若在定義類別時,希望某方法不被拿來作為綁定方法,可以使用 @staticmethod
加以標註。例如:
class Account:
…略
@staticmethod
def default(name, number):
return Account(name, number, 100)
這麼一來,可以使用 Account.default('Monica', '765-4321')
這樣的方式來呼叫它,就算使用者透過類別的實例來呼叫它,像是 acct.default('Monica', '765-4321')
,acct
也不會被傳入作為 default
的第一個參數。
雖然可以透過實例來呼叫 @staticmethod
標註的方法,但建議透過類別名稱來呼叫,明確地讓類別名稱作為靜態方法的名稱空間。
類別方法
來仔細看看上面的例子,default
方法中寫死了 Account
這個名稱,萬一要修改類別名稱的話,還要記得修改 default
中的類別名稱,我們可以讓 default
的實作更有彈性。
首先得知道的是,在 Python 中定義的類別,也會產生對應的物件,這個物件會是 type
的實例。例如:
>>> class Some:
... pass
...
>>> Some
<class '__main__.Some'>
>>> type(Some)
<class 'type'>
>>> s = Some()
>>> s.__class__
<class '__main__.Some'>
>>> s.__class__()
<__main__.Some object at 0x00000212ED2C9430>
>>>
可以看到,也可以使用物件的 __class__
屬性來得知,該物件是從哪個類別建構而來,也可以透過取得的 type
實例來建構物件。
因此,只要能在先前的 default
方法中,取得目前所在類別的 type
實例,就可以不用寫死類別名稱了,對於這個需求,可以在 default
方法上標註 @classmethod
。例如:
class Account:
…略
@classmethod
def default(cls, name: str, number: str):
return cls(name, number, 100)
類別中的方法若標註了 @classmethod
,第一個參數一定是接受所在類別的 type
實例,因此,在 default
方法中,就可以使用第一個參數來建構物件。