流暢的python讀書筆記-第11章-接口:從協議到抽象基類

抽象基類

抽象基類的常見用途:

  1. 實現接口時做爲超類使用。
  2. 而後,說明抽象基類如何檢查具體子類是否符合接口定義,以及如何使用註冊機制聲明一個類實現了某個接口,而不進行子類化操做。
  3. 如何讓抽象基類自動「識別」任何符合接口的類——不進行子類化或註冊。

接口在動態類型語言中是怎麼運做的呢?

  1. 按照定義,受保護的屬性和私有屬性不在接口中:
  2. 即使「受保護的」屬性也只是採用命名約定實現的(單個前導下劃線)
  3. 私有屬性能夠輕鬆地訪問(參見 9.7 節),緣由也是如此。 不要違背這些約定。
  4. 不要以爲把公開數據屬性放入對象的接口中不妥,
  5. 由於若是須要,總能實現讀值方法和設值方法,把數據屬性變成特性,使用 obj.attr 句法的客戶代碼不會受到影響。

Python喜歡序列

  1. 協議是接口,但不是正式的(只由文檔和約定定義),
  2. 所以協議不能像正式接口那樣施加限制(本章後面會說明抽象基類對接口一致性的強制)。
  3. 一個類可能只實現部分接口,這是容許的。

看看示例 11-3 中的 Foo 類。它沒有繼承 abc.Sequence,並且只實現了序列協議
的一個方法: getitem (沒有實現 len 方法)app

定義 getitem 方法,只實現序列協議的一部分,這樣足夠訪問元
素、迭代和使用 in 運算符了
>>> class Foo:
... def __getitem__(self, pos):
... return range(0, 30, 10)[pos]
...
>>> f = Foo()
>>> f[1]
10
>>> for i in f: print(i)
...
0
10
20
>>> 20 in f
True
>>> 15 in f
False

綜上,鑑於序列協議的重要性,若是沒有 itercontains 方法,Python 會調
getitem 方法,設法讓迭代和 in 運算符可用。框架

使用猴子補丁在運行時實現協議

random.shuffle 函數打亂 FrenchDeck 實例dom

爲FrenchDeck 打猴子補丁,把它變成可變的,讓 random.shuffle 函
數能處理ssh

def set_card(deck, position, card): ➊
... deck._cards[position] = card
>>> FrenchDeck.__setitem__ = set_card ➋
>>> shuffle(deck) ➌
>>> deck[:5]
[Card(rank='3', suit='hearts'), Card(rank='4', suit='diamonds'), Card(rank='4',
suit='clubs'), Card(rank='7', suit='hearts'), Card(rank='9', suit='spades')]

❶ 定義一個函數,它的參數爲 deck、position 和 card。
❷ 把那個函數賦值給 FrenchDeck 類的 setitem 屬性。
❸ 如今能夠打亂 deck 了,由於 FrenchDeck 實現了可變序列協議所需的方法。函數

這裏的關鍵是,set_card 函數要知道 deck 對象有一個名爲 _cards 的屬性,並且
_cards 的值必須是可變序列。
而後,咱們把 set_card 函數賦值給特殊方法__setitem__,從而把它依附到 FrenchDeck 類上。
這種技術叫猴子補丁:在運行時修改類或模塊,而不改動源碼。

協議是動態的

  1. random.shuffle 函數不關心參數的類型,只要那個對象實現了部分可變序列協議便可。
  2. 即使對象一開始沒有所需的方法也不要緊,後來再提供也行

抽象基類使用姿式

有時,爲了讓抽象基類識別子類,甚至不用註冊。
其實,抽象基類的本質就是幾個特殊方法。測試

>>> class Struggle:
... def __len__(self): return 23
...
>>> from collections import abc
>>> isinstance(Struggle(), abc.Sized)
True
能夠看出,無需註冊,abc.Sized 也能把 Struggle 識別爲本身的子類,只要實現
了特殊方法 len 便可(要使用正確的句法和語義實現,前者要求沒有參數,後
者要求返回一個非負整數,指明對象的長度;

做者建議

若是實現的類體現了 numbers、collections.abc 或其餘框架中
抽象基類的概念,
要麼繼承相應的抽象基類(必要時),要麼把類註冊到相應的抽象
基類中。
開始開發程序時,不要使用提供註冊功能的庫或框架,要本身動手註冊網站

一句話:
1.要麼繼承基類
2.要麼本身把類註冊到相應的抽象基類中 ,別使用自動註冊ui

isinstance 檢查使用姿式

然而,即使是抽象基類,也不能濫用 isinstance 檢查,用得多了可能致使代碼異味,即代表面向對象設計得很差。編碼

在一連串 if/elif/elif 中使用 isinstance 作檢查,而後根據對象的類型執行不一樣的操做,一般是很差的作法;spa

此時應該使用多態,即採用必定的方式定義類,讓解釋器把調用分派給正確的方法,而不使用 if/elif/elif 塊硬編碼分派邏輯。

鴨子類型 和 類型檢查

在框架以外,鴨子類型一般比類型檢查更簡單,也更靈活。

  1. 本書有幾個示例要使用序列,把它當成列表處理。
  2. 我沒有檢查參數的類型是否是list,而是直接接受參數,當即使用它構建一個列表。
  3. 這樣,我就能夠接受任何可迭代對象;
  4. 若是參數不是可迭代對象,調用當即失敗,而且提供很是清晰的錯誤消息。

一句話:
看起來像鴨子(如序列),直接用序列的特性方法,(若是爆錯就是類型不對),若是能夠就是經過

這種作法省去了,用isinstance 作檢查的痛苦(有時不知道什麼類型)

標準庫中的抽象基類急順序 page 375 376

定義並使用一個抽象基類

重點來了

想象一下這個場景:

你要在網站或移動應用中顯示隨機廣告,可是在整個廣告清單輪轉一遍以前,不重複顯示
廣告。

假設咱們在構建一個廣告管理框架,名爲 ADAM。

它的職責之一是,支持用戶提供隨機挑選的無重複類。

爲了讓 ADAM 的用戶明確理解「隨機挑選的無重複」組件是什麼意思,咱們將定義一個抽象基類。

我將使用現實世界中的物品命名這個抽象基類:賓果機和彩票機是隨機從有限的集合中挑選物品的機器,選出的物品沒有重複,直到選完爲止

Tombola 抽象基類有四個方法,其中兩個是抽象方法。

  • .load(...):把元素放入容器。
  • .pick():從容器中隨機拿出一個元素,返回選中的元素。

另外兩個是具體方法。

  • .loaded():若是容器中至少有一個元素,返回 True。
  • .inspect():返回一個有序元組,由容器中的現有元素構成,不會修改容器的內容 (內部的順序不保留)。

clipboard.png

代碼:

import abc


class Tombola(abc.ABC):
    @abc.abstractmethod
    def load(self, iterable):
        """從可迭代對象中添加元素。"""

    @abc.abstractmethod
    def pick(self):
        """隨機刪除元素,而後將其返回。
        若是實例爲空,這個方法應該拋出`LookupError`。
        """

    def loaded(self):
        """若是至少有一個元素,返回`True`,不然返回`False`。"""
        return bool(self.inspect())

    def inspect(self):
        """返回一個有序元組,由當前元素構成。"""
        items = []
        while True:
            try:
                items.append(self.pick())
            except LookupError:
                break
        self.load(items)
        return tuple(sorted(items))

本身定義的抽象基類要繼承 abc.ABC。
根據文檔字符串,若是沒有元素可選,應該拋出 LookupError。
❹ 抽象基類能夠包含具體方法。
❻ 咱們不知道具體子類如何存儲元素,不過爲了獲得 inspect 的結果,咱們能夠不斷調
用 .pick() 方法,把 Tombola 清空……
❼ ……而後再使用 .load(...) 把全部元素放回去。

其實,抽象方法能夠有實現代碼。即使實現了,子類也必須覆蓋抽象方法,但
是在子類中可使用 super() 函數調用抽象方法,爲它添加功能,而不是從頭開始
實現。

定義Tombola抽象基類的子類

BingoCage 類是在示例 5-8 的基礎上修改的,使用了更好的隨機發生
器。
BingoCage 實現了所需的抽象方法 load 和 pick,從 Tombola 中繼承了 loaded 方
法,覆蓋了 inspect 方法,還增長了 call 方法。

import abc


class Tombola(abc.ABC):
    @abc.abstractmethod
    def load(self, iterable):
        """從可迭代對象中添加元素。"""

    @abc.abstractmethod
    def pick(self):
        """隨機刪除元素,而後將其返回。
        若是實例爲空,這個方法應該拋出`LookupError`。
        """

    def loaded(self):
        """若是至少有一個元素,返回`True`,不然返回`False`。"""
        return bool(self.inspect())

    def inspect(self):
        """返回一個有序元組,由當前元素構成。"""
        items = []
        while True:
            try:
                items.append(self.pick())
            except LookupError:
                break
        self.load(items)
        return tuple(sorted(items))


import random

class BingoCage(Tombola):
    def __init__(self, items):
        self._randomizer = random.SystemRandom()
        self._items = []
        self.load(items)

    def load(self, items):
        self._items.extend(items)
        self._randomizer.shuffle(self._items)

    def pick(self):
        try:
            return self._items.pop()
        except IndexError:
            raise LookupError('pick from empty BingoCage')

    def __call__(self):
        self.pick()

❹ 沒有使用 random.shuffle() 函數,而是使用 SystemRandom 實例的 .shuffle() 方法。
這裏想表達的觀點是:咱們能夠偷懶,直接從抽象基類中繼承不是那麼理想的具體方法。

從 Tombola 中繼承的方法沒有BingoCage 本身定義的那麼快,不過只要 Tombola 的子類正確實現 pick 和 load 方法,就能提供正確的結果。

LotteryBlower 打亂「數字球」後沒有取出最後一個,而是取出一個隨機位置上的

球。

❷ 若是範圍爲空,random.randrange(...) 函數拋出 ValueError,爲了兼容
Tombola,咱們捕獲它,拋出 LookupError。

❹ 覆蓋 loaded 方法,避免調用 inspect 方法(示例 11-9 中的 Tombola.loaded 方法是
這麼作的)。咱們能夠直接處理 self._balls 而沒必要構建整個有序元組,從而提高速
度。

有個習慣作法值得指出:

  • init 方法中,self._balls 保存的是list(iterable),而不是 iterable 的引用(即沒有直接把iterable 賦值給self._balls)。
  • 前面說過, 這樣作使得 LotteryBlower 更靈活,由於 iterable 參數能夠是任何可迭代的類型。
  • 把元素存入列表中還確保能取出元素。
  • 就算 iterable 參數始終傳入列表,list(iterable)
    會建立參數的副本,這依然是好的作法,由於咱們要從中刪除元素,而客戶可能不但願本身提供的列表被修改。

Tombola的虛擬子類

  1. 註冊虛擬子類的方式是在抽象基類上調用 register 方法。這麼作以後,註冊的類會變成抽象基類的虛擬子類,
  2. 並且 issubclass 和 isinstance 等函數都能識別,可是註冊的類不會從抽象基類中繼承任何方法或屬性。

3.虛擬子類不會繼承註冊的抽象基類,爲了不運行時錯誤,虛擬子類要實現所需的所有方法。

import abc


class Tombola(abc.ABC):
    @abc.abstractmethod
    def load(self, iterable):
        """從可迭代對象中添加元素。"""

    @abc.abstractmethod
    def pick(self):
        """隨機刪除元素,而後將其返回。
        若是實例爲空,這個方法應該拋出`LookupError`。
        """

    def loaded(self):
        """若是至少有一個元素,返回`True`,不然返回`False`。"""
        return bool(self.inspect())

    def inspect(self):
        """返回一個有序元組,由當前元素構成。"""
        items = []
        while True:
            try:
                items.append(self.pick())
            except LookupError:
                break
        self.load(items)
        return tuple(sorted(items))


import random


class BingoCage(Tombola):
    def __init__(self, items):
        self._randomizer = random.SystemRandom()
        self._items = []
        self.load(items)

    def load(self, items):
        self._items.extend(items)
        self._randomizer.shuffle(self._items)

    def pick(self):
        try:
            return self._items.pop()
        except IndexError:
            raise LookupError('pick from empty BingoCage')

    def __call__(self):
        self.pick()


class LotteryBlower(Tombola):
    def __init__(self, iterable):
        self._balls = list(iterable)

    def load(self, iterable):
        self._balls.extend(iterable)

    def pick(self):
        try:
            position = random.randrange(len(self._balls))
        except ValueError:
            raise LookupError('pick from empty lotteryBlower')

    def loaded(self):
        return bool(self._balls)

    def inspect(self):
        return tuple(sorted(self._balls))


from random import randrange


@Tombola.register
class TomboList(list):
    def pick(self):
        if self:
            position = randrange(len(self))
            return self.pop(position)
        else:
            raise LookupError('pop from empty TomboList')

    load = list.extend

    def loaded(self):
        return bool(self)

    def inspect(self):
        return tuple(sorted(self))


# Tombola.register(TomboList)

把 Tombolist 註冊爲 Tombola 的虛擬子類。
❸ Tombolist 從 list 中繼承 bool 方法,列表不爲空時返回 True。
❹ pick 調用繼承自 list 的 self.pop 方法,傳入一個隨機的元素索引。

註冊以後,可使用 issubclass 和 isinstance 函數判斷 TomboList 是否是Tombola的子類:

>>> from tombola import Tombola
>>> from tombolist import TomboList
>>> issubclass(TomboList, Tombola)
True
>>> t = TomboList(range(100))
>>> isinstance(t, Tombola)
True

Tombola子類的測試方法

__subclasses__()
  這個方法返回類的直接子類列表,不含虛擬子類。
_abc_registry
  只有抽象基類有這個數據屬性,其值是一個 WeakSet 對象,即抽象類註冊的虛擬子
類的弱引用。

Python使用register的方式

Tombola.register 看成類裝飾器使用。在 Python 3.3 以前的版本中不能這
樣使用 register

雖然如今能夠把 register 看成裝飾器使用了,但更常見的作法仍是把它看成函數使用,
用於註冊其餘地方定義的類。

即使不註冊,抽象基類也能把一個類識別爲虛擬子類

>>> class Struggle:
... def __len__(self): return 23
...
>>> from collections import abc
>>> isinstance(Struggle(), abc.Sized)
True
>>> issubclass(Struggle, abc.Sized)
True
  1. issubclass 函數確認(isinstance 函數也會得出相同的結論)
  2. Struggle 是abc.Sized 的子類,
  3. 這是由於 abc.Sized 實現了一個特殊的類方法,名爲__subclasshook__。
Sized 類的源碼:
class Sized(metaclass=ABCMeta):
 __slots__ = ()
 @abstractmethod
 def __len__(self):
 return 0
 @classmethod
 def __subclasshook__(cls, C):
 if cls is Sized:
 if any("__len__" in B.__dict__ for B in C.__mro__): # ➊
 return True # ➋
 return NotImplemented # ➌

對 C.__mro__ (即 C 及其超類)中所列的類來講,若是類的 dict 屬性中有名爲
len 的屬性……

小結

1.抽象基類的使用姿式
2.定義一個隨機抽象基類
3.虛擬子類 只是註冊就行,(沒繼承),必須實現全部方法
4.Tombola 這個自定義的抽象基類多寫幾回

其餘:

非正式接口(稱爲協議)的高度動態本性,
以及使用 subclasshook 方法動態識別子類。

咱們發現 Python 對序列協議的支持十分深刻。
若是一個類實現了__getitem__ 方法,此外什麼也沒作,那麼 Python 會設法迭代它,並且 in 運算符也隨之可使用。

顯式繼承抽象基類的優缺點。
繼承abc.MutableSequence 後,必須實現 insert 和 delitem 方法,而咱們並不須要這兩個方法。

相關文章
相關標籤/搜索