看看示例 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
綜上,鑑於序列協議的重要性,若是沒有 iter 和 contains 方法,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 類上。
這種技術叫猴子補丁:在運行時修改類或模塊,而不改動源碼。
有時,爲了讓抽象基類識別子類,甚至不用註冊。
其實,抽象基類的本質就是幾個特殊方法。測試
>>> 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 檢查,用得多了可能致使代碼異味,即代表面向對象設計得很差。編碼
在一連串 if/elif/elif 中使用 isinstance 作檢查,而後根據對象的類型執行不一樣的操做,一般是很差的作法;spa
此時應該使用多態,即採用必定的方式定義類,讓解釋器把調用分派給正確的方法,而不使用 if/elif/elif 塊硬編碼分派邏輯。
在框架以外,鴨子類型一般比類型檢查更簡單,也更靈活。
一句話:
看起來像鴨子(如序列),直接用序列的特性方法,(若是爆錯就是類型不對),若是能夠就是經過
這種作法省去了,用isinstance 作檢查的痛苦(有時不知道什麼類型)
重點來了
你要在網站或移動應用中顯示隨機廣告,可是在整個廣告清單輪轉一遍以前,不重複顯示
廣告。
假設咱們在構建一個廣告管理框架,名爲 ADAM。
它的職責之一是,支持用戶提供隨機挑選的無重複類。
爲了讓 ADAM 的用戶明確理解「隨機挑選的無重複」組件是什麼意思,咱們將定義一個抽象基類。
我將使用現實世界中的物品命名這個抽象基類:賓果機和彩票機是隨機從有限的集合中挑選物品的機器,選出的物品沒有重複,直到選完爲止
另外兩個是具體方法。
代碼:
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() 函數調用抽象方法,爲它添加功能,而不是從頭開始
實現。
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 方法,就能提供正確的結果。
球。
❷ 若是範圍爲空,random.randrange(...) 函數拋出 ValueError,爲了兼容
Tombola,咱們捕獲它,拋出 LookupError。
❹ 覆蓋 loaded 方法,避免調用 inspect 方法(示例 11-9 中的 Tombola.loaded 方法是
這麼作的)。咱們能夠直接處理 self._balls 而沒必要構建整個有序元組,從而提高速
度。
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
__subclasses__()
這個方法返回類的直接子類列表,不含虛擬子類。
_abc_registry
只有抽象基類有這個數據屬性,其值是一個 WeakSet 對象,即抽象類註冊的虛擬子
類的弱引用。
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
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 方法,而咱們並不須要這兩個方法。