翻譯:《實用的Python編程》05_02_Classes_encapsulation
目錄 | 上一節 (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__ 。
浙公網安備 33010602011771號