翻譯:《實用的Python程式設計》05_02_Classes_encapsulation

codists發表於2021-03-13

目錄 | 上一節 (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 = 50shares 賦值的,那麼現在就要改成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__

目錄 | 上一節 (5.1 再談字典) | 下一節 (6 生成器)

注:完整翻譯見 https://github.com/codists/practical-python-zh

相關文章