目錄 | 上一節 (5.1 再談字典) | 下一節 (6 生成器)
5.2 類和封裝
建立類時,通常會嘗試將類的內部細節進行封裝。本節介紹 Python 程式設計中有關封裝的習慣用法(包括私有變數和私有屬性)。
Public vs Private
雖然類的主要作用之一是封裝物件的屬性和內部實現細節。但是,類還定義了外界用來操作該物件的公有介面(public interface)。實現細節與公有介面之間的區別很重要。
問題
在 Python 中,幾乎所有與類和物件有關的東西都是開放(open)的。
- 可以輕鬆地檢視物件的內部細節。
- 可以隨意地修改。
- 沒有訪問控制的概念(例如:私有類成員)。
如何隔離內部實現的細節,這是一個問題。
Python 封裝
Python 依賴程式設計約定來指示某些東西的用途。這就約定基於命名。有一種普遍的態度是,程式設計師應該遵守規則,而不是讓語言來強制執行規則。
私有屬性
以下劃線 _
開頭的任何屬性被認為是私有的(private)。
class Person(object):
def __init__(self, name):
self._name = 0
如前所述,這這是一種程式設計風格。你仍然可以對這些私有屬性進行訪問和修改。
>>> p = Person('Guido')
>>> p._name
'Guido'
>>> p._name = 'Dave'
>>>
一般來說,一個以下劃線 _
開頭的名稱被認為是內部實現,無論該名稱是變數名、函式名還是模組名。如果你發現自己直接使用這些名稱,那麼你可能在做一些錯誤的事情。你應該尋找更高階的功能。
簡單屬性
考慮下面這個類:
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
這裡有一個讓人驚訝的特性,你可以給屬性設定任何值:
>>> s = Stock('IBM', 50, 91.1)
>>> s.shares = 100
>>> s.shares = "hundred"
>>> s.shares = [1, 0, 0]
>>>
你可能會想要對此進行檢查(譯註:例如 shares
表示的是股份數目,值應該是整數。所以給 shares
賦值時應該對值進行檢查。如果檢查發現給 shares
賦的值不是整數,那麼應該觸發一個 TypeError
異常):
s.shares = '50' # Raise a TypeError, this is a string
這時候你會怎麼做?
託管屬性
方法一:引進訪問方法(accessor methods)。
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.set_shares(shares)
self.price = price
# Function that layers the "get" operation
def get_shares(self):
return self._shares
# Function that layers the "set" operation
def set_shares(self, value):
if not isinstance(value, int):
raise TypeError('Expected an int')
self._shares = value
糟糕的是,這破壞了我們的已有程式碼。例如:之前是通過 s.shares = 50
給 shares
賦值的,那麼現在就要改成s.set_shares(50)
給 shares
賦值,這很不好。
特徵屬性(Properties)
方法二:
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
@property
def shares(self):
return self._shares
@shares.setter
def shares(self, value):
if not isinstance(value, int):
raise TypeError('Expected int')
self._shares = value
現在,普通屬性(normal attribute)的訪問觸發了 @property
和 @shares.setter
下的 getter 方法和 setter 方法。
>>> s = Stock('IBM', 50, 91.1)
>>> s.shares # Triggers @property
50
>>> s.shares = 75 # Triggers @shares.setter
>>>
使用該方法,不需要對原始碼做任何修改。在類內(包括在 __init__()
方法內)有賦值的時候,直接呼叫新的 setter:
class Stock:
def __init__(self, name, shares, price):
...
# This assignment calls the setter below
self.shares = shares
...
...
@shares.setter
def shares(self, value):
if not isinstance(value, int):
raise TypeError('Expected int')
self._shares = value
特徵屬性和私有名稱( private names)的使用之間經常會出現混淆。儘管特徵屬性內部使用的是私有名稱,如 _shares
。類的其它地方(不是特徵屬性),仍可以繼續使用諸如 shares
這樣的名稱。
特徵屬性對於計算資料屬性也非常有用。
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
@property
def cost(self):
return self.shares * self.price
...
這允許你刪除 cost 後面的括號,隱藏 cost 是一個方法的事實:
>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares # Instance variable
100
>>> s.cost # Computed Value
49010.0
>>>
統一訪問
最後一個例子展示瞭如何在物件上放置一個更加統一的介面。如果不這樣做,物件使用起來可能會令人困惑。
>>> s = Stock('GOOG', 100, 490.1)
>>> a = s.cost() # Method
49010.0
>>> b = s.shares # Data attribute
100
>>>
為什麼 cost 後面需要加上括號 ()
,但是 shares 卻不需要? 特徵屬性可以解決這個問題。
裝飾器語法
@
語法稱為“裝飾(decoration)”。它指定了一個修飾符(modifier),應用於緊接其後的函式定義:
...
@property
def cost(self):
return self.shares * self.price
更多細節在 第 7 節 中給到。
插槽屬性(__slots__
)
你可以使用 __slots__
限制屬性名稱集:
class Stock:
__slots__ = ('name','_shares','price')
def __init__(self, name, shares, price):
self.name = name
...
使用其它屬性時,將會觸發錯誤:
>>> s.price = 385.15
>>> s.prices = 410.2
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: 'Stock' object has no attribute 'prices'
管這樣可以防止錯誤和限制物件的使用,但實際上使用 __slots__
是為了提高效能,提高 Python 利用記憶體的效率。
關於封裝的最終說明
不要濫用私有屬性(private attributes),特徵屬性(properties),插槽屬性(slots)等。它們有特殊的用途,你在閱讀其它 Python 程式碼時可能會看到。但是,對於大多數日常編碼而言,它們不是必需的。
練習
練習 5.6:簡單特徵屬性
使用特徵屬性是一種非常有用的給物件新增“計算屬性”的方式。雖然你在 stock.py
檔案中建立了 Stock
物件,但是請注意,在 Stock
物件上 ,對於不同型別的屬性,獲取方式稍微有點不同。
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares
100
>>> s.price
490.1
>>> s.cost()
49010.0
>>>
具體來說,cost
後面之所以要新增括號,是因為 cost
是一個方法。
如果你想去掉 cost()
的括號,那麼可以把該方法轉為一個特徵屬性。請修改 Stock
類,使其像下面這樣計算所持有股票的總價:
>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.cost
49010.0
>>>
嘗試將 cost
作為方法呼叫(s.cost()
),你會發現,現在已經被定義為特徵屬性的 cost
無法作為方法被呼叫。
>>> s.cost()
... fails ...
>>>
這些更改很可能會破壞你之前的 pcost.py
程式,所以,你可能需要返回到 pcost.py
中去掉 cost()
方法後面的括號()
。
練習 5.7:特徵屬性和 Setters
請修改 shares
屬性,以便將該值儲存在私有屬性中,並且使用屬性函式(property functions)確保賦給 shares
的值總是整數。預期行為示例:
>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG',100,490.10)
>>> s.shares = 50
>>> s.shares = 'a lot'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: expected an integer
>>>
練習 5.8:新增插槽屬性(slots)
請修改 Stock
類,以便 Stock
類擁有一個 __slots__
屬性。然後確認無法新增新屬性:
>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.10)
>>> s.name
'GOOG'
>>> s.blah = 42
... see what happens ...
>>>
使用 __slots__
時,Python 使用更高效的物件內部表示。如果你嘗試檢視例項 s
的底層字典會發生什麼?
>>> s.__dict__
... see what happens ...
>>>
應當指出, __slots__
作為資料結構是類中最常用的一種優化。使用插槽屬性使程式佔用更少的記憶體,執行更快。但是,在其它大多數類中,你應該儘可能避免使用 __slots__
。