摘要:自己定義的抽象基類要繼承。抽象基類可以包含具體方法。這里想表達的觀點是我們可以偷懶,直接從抽象基類中繼承不是那么理想的具體方法。
抽象基類 抽象基類的常見用途:
實現接口時作為超類使用。
然后,說明抽象基類如何檢查具體子類是否符合接口定義,以及如何使用注冊機制聲明一個類實現了某個接口,而不進行子類化操作。
如何讓抽象基類自動“識別”任何符合接口的類——不進行子類化或注冊。
接口在動態類型語言中是怎么運作的呢?按照定義,受保護的屬性和私有屬性不在接口中:
即便“受保護的”屬性也只是采用命名約定實現的(單個前導下劃線)
私有屬性可以輕松地訪問(參見 9.7 節),原因也是如此。 不要違背這些約定。
不要覺得把公開數據屬性放入對象的接口中不妥,
因為如果需要,總能實現讀值方法和設值方法,把數據屬性變成特性,使用 obj.attr 句法的客戶代碼不會受到影響。
Python喜歡序列協議是接口,但不是正式的(只由文檔和約定定義),
因此協議不能像正式接口那樣施加限制(本章后面會說明抽象基類對接口一致性的強制)。
一個類可能只實現部分接口,這是允許的。
看看示例 11-3 中的 Foo 類。它沒有繼承 abc.Sequence,而且只實現了序列協議
的一個方法: getitem (沒有實現 len 方法)
定義 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 實例
為FrenchDeck 打猴子補丁,把它變成可變的,讓 random.shuffle 函
數能處理
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 類上。
這種技術叫猴子補丁:在運行時修改類或模塊,而不改動源碼。
random.shuffle 函數不關心參數的類型,只要那個對象實現了部分可變序列協議即可。
即便對象一開始沒有所需的方法也沒關系,后來再提供也行
抽象基類使用姿勢有時,為了讓抽象基類識別子類,甚至不用注冊。
其實,抽象基類的本質就是幾個特殊方法。
>>> 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.要么自己把類注冊到相應的抽象基類中 ,別使用自動注冊
然而,即便是抽象基類,也不能濫用 isinstance 檢查,用得多了可能導致代碼異味,即表明面向對象設計得不好。
在一連串 if/elif/elif 中使用 isinstance 做檢查,然后根據對象的類型執行不同的操作,通常是不好的做法;
此時應該使用多態,即采用一定的方式定義類,讓解釋器把調用分派給正確的方法,而不使用 if/elif/elif 塊硬編碼分派邏輯。
鴨子類型 和 類型檢查在框架之外,鴨子類型通常比類型檢查更簡單,也更靈活。
本書有幾個示例要使用序列,把它當成列表處理。
我沒有檢查參數的類型是不是list,而是直接接受參數,立即使用它構建一個列表。
這樣,我就可以接受任何可迭代對象;
如果參數不是可迭代對象,調用立即失敗,并且提供非常清晰的錯誤消息。
一句話:
看起來像鴨子(如序列),直接用序列的特性方法,(如果爆錯就是類型不對),如果可以就是通過
這種做法省去了,用isinstance 做檢查的痛苦(有時不知道什么類型)
標準庫中的抽象基類急順序 page 375 376 定義并使用一個抽象基類重點來了
想象一下這個場景:你要在網站或移動應用中顯示隨機廣告,但是在整個廣告清單輪轉一遍之前,不重復顯示
廣告。
假設我們在構建一個廣告管理框架,名為 ADAM。
它的職責之一是,支持用戶提供隨機挑選的無重復類。
為了讓 ADAM 的用戶明確理解“隨機挑選的無重復”組件是什么意思,我們將定義一個抽象基類。
我將使用現實世界中的物品命名這個抽象基類:賓果機和彩票機是隨機從有限的集合中挑選物品的機器,選出的物品沒有重復,直到選完為止Tombola 抽象基類有四個方法,其中兩個是抽象方法。
.load(...):把元素放入容器。
.pick():從容器中隨機拿出一個元素,返回選中的元素。
另外兩個是具體方法。
.loaded():如果容器中至少有一個元素,返回 True。
.inspect():返回一個有序元組,由容器中的現有元素構成,不會修改容器的內容 (內部的順序不保留)。
代碼:
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(...) 把所有元素放回去。
其實,抽象方法可以有實現代碼。即便實現了,子類也必須覆蓋抽象方法,但定義Tombola抽象基類的子類
是在子類中可以使用 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 方法,就能提供正確的結果。
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)
會創建參數的副本,這依然是好的做法,因為我們要從中刪除元素,而客戶可能不希望自己提供的列表被修改。
注冊虛擬子類的方式是在抽象基類上調用 register 方法。這么做之后,注冊的類會變成抽象基類的虛擬子類,
而且 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) TrueTombola子類的測試方法
__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
issubclass 函數確認(isinstance 函數也會得出相同的結論)
Struggle 是abc.Sized 的子類,
這是因為 abc.Sized 實現了一個特殊的類方法,名為__subclasshook__。
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 方法,而我們并不需要這兩個方法。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/41771.html
摘要:繼承的優缺點推出繼承的初衷是讓新手順利使用只有專家才能設計出來的框架。多重繼承的真實應用多重繼承能發揮積極作用。即便是單繼承,這個原則也能提升靈活性,因為子類化是一種緊耦合,而且較高的繼承樹容易倒。 繼承的優缺點 推出繼承的初衷是讓新手順利使用只有專家才能設計出來的框架。——Alan Kay 子類化內置類型很麻煩 (如 list 或 dict)) ,別搞這種 直接子類化內置類型(如 ...
摘要:本篇內容將從鴨子類型的動態協議,逐漸過渡到使接口更明確能驗證實現是否符合規定的抽象基類。抽象基類介紹完動態實現接口后,現在開始討論抽象基類,它屬于靜態顯示地實現接口。標準庫中的抽象基類從開始,標準庫提供了抽象基類。 《流暢的Python》筆記。本篇是面向對象慣用方法的第四篇,主要討論接口。本篇內容將從鴨子類型的動態協議,逐漸過渡到使接口更明確、能驗證實現是否符合規定的抽象基類(Abst...
摘要:本文重點協議是中非正式的接口了解抽象基類的基本概念以及標準庫中的抽象基類掌握抽象基類的使用方法。三抽象基類的使用通過繼承聲明抽象基類聲明抽象基類最簡單的方式是繼承或其他抽象基類注意在之間,繼承抽象基類的語法是。 導語:本文章記錄了本人在學習Python基礎之面向對象篇的重點知識及個人心得,打算入門Python的朋友們可以來一起學習并交流。 本文重點: 1、協議是Python中非正式的接...
摘要:在復雜的情況下,需要具體策略維護內部狀態時,可能需要把策略和享元模式結合起來。函數比用戶定義的類的實例輕量,而且無需使用享元模式,因為各個策略函數在編譯模塊時只會創建一次。 一等函數實現設計模式 經典的策略模式定義 定義一系列算法,把它們一一封裝起來,并且使它們可以相互替換。本模式使得算法可以獨立于使用它的客戶而變化。 案例 假如一個網店制定了下述折扣規則。 有 1000 或以上積分...
摘要:例如,的序列協議只需要和兩個方法。任何類如,只要使用標準的簽名和語義實現了這兩個方法,就能用在任何期待序列的地方。方法開放了內置序列實現的棘手邏輯,用于優雅地處理缺失索引和負數索引,以及長度超過目標序列的切片。 序列的修改、散列和切片 接著造Vector2d類 要達到的要求 為了編寫Vector(3, 4) 和 Vector(3, 4, 5) 這樣的代碼,我們可以讓 init 法接受任...
閱讀 2106·2021-11-05 09:42
閱讀 2851·2021-09-23 11:21
閱讀 2841·2019-08-30 14:00
閱讀 3314·2019-08-30 13:15
閱讀 465·2019-08-29 17:18
閱讀 3547·2019-08-29 16:29
閱讀 2749·2019-08-29 14:06
閱讀 2794·2019-08-23 14:41