《流暢的Python》筆記。本篇是「面向對象慣用方法」的第四篇,主要討論接口。本篇內容將從鴨子類型的動態協議,逐漸過渡到使接口更明確、能驗證明現是否符合規定的抽象基類(Abstract Base Class, ABC)。python
本篇討論Python中接口的實現問題,主要內容以下:程序員
補充在正文以前:bash
猴子補丁並非Python特有,它指動態語言中,不用修改源代碼,在運行時就能對代碼的功能進行動態的追加或變動。下面的代碼展現了猴子補丁的用法:微信
# 代碼2.1 # 在文件中定義 class MyList: def __init__(self, iterable): self._data = list(iterable) def __len__(self): return len(self._data) def __getitem__(self, index): return self._data[index] # 下面的代碼在控制檯運行 >>> from random import shuffle >>> from my_list import MyList >>> mylist = MyList(range(10)) >>> def set_item(temp, i, item): ... temp._data[i] = item ... >>> MyList.__setitem__ = set_item >>> shuffle(mylist) >>> deck[:] [6, 3, 0, 1, 5, 4, 2, 7, 9, 8]
解釋:app
random.shuffle
函數,對象必須實現__setitem__
方法,上述代碼在運行時動態添加所需方法;set_item
函數的第一個參數並非self
,這是想說明,每一個Python方法說到底都是普通函數,把第一個參數命名爲self
只是一種約定(但別隨意打破這種約定)。這裏之因此講猴子補丁,主要是爲了說明協議能夠是動態的:即便對象最初沒有實現某個協議,當須要時,咱們也能爲它動態添加。框架
介紹完動態實現接口後,如今開始討論抽象基類,它屬於靜態顯示地實現接口。dom
有時候咱們須要明確區分「抽象類」(並非指「抽象基類」)與「接口」:以天然界爲例,「抽象類」通常用於同一物種同一行爲,而「接口」則用於不一樣物種同一行爲。固然,這兩個概念有交叉的部分,某些行爲既能夠歸到「接口「,也能夠歸到」抽象類「,而最後歸到誰就見仁見智了。但這兩個概念又有很大的類似之處,它們的實質都是:讓某些對象擁有同名的方法或屬性,但具體實現不必定相同。ssh
Java更注重這二者的特性,而Python、C++則更注重這二者的共性。也所以,Java不支持多重繼承(固然,也是爲了下降複雜性),用明確的接口類interface
來區分與abstract class
;而在Python和C++中,則用抽象基類充當接口。因此,在Python中,直接繼承自抽象基類,更多代表的是」要實現某種接口或協議「,而非」要新建某個具體類的子類「。函數
若是要測試是否繼承自抽象基類,推薦使用isinstance
和issubclass
方法,而不是is
運算。但也不要濫用這類方法,由於這種代碼用多了說明面向對象設計得很差。測試
說道isinstance
,還有個與之相關的概念,至關於「鴨子類型」的強化版:
cls
是抽象基類,即cls
的元素是abc.ABCMeta
,就可使用isinstance(obj, cls)
。小插曲:這是書中給出的標準定義,筆者讀到這的時候一臉懵逼。「白鵝類型」是個名詞,但這定義倒是對一個過程的描述,因此「白鵝類型」究竟是個啥(這究竟是翻譯的鍋仍是做者的鍋)?後來谷歌了一下,再本身反覆推敲,得出以下總結:鴨子類型是指某個實例實現了某個方法,就能夠說它屬於某個類型,不必定要繼承;而白鵝類型則是指能被斷定成某抽象基類的子類的實例,即,能使isinstance(obj, cls)
返回True
的obj
就是白鵝類型,其中cls
是抽象基類。注意,這些子類並不必定是經過繼承而來,也多是經過註冊而來,還多是經過實現某些方法而來。
特別提醒:對於抽象基類(還有元類)的使用,並不建議在生產代碼中自行定義新的抽象基類和元類。定義抽象基類和元類的工做通常由比較資深的Python程序員來作,適用於寫框架的程序員。而即使是資深Python程序員也不常本身定義抽象基類和元類。
從Python2.6開始,標準庫提供了抽象基類。大多數抽象基類在collections.abc
模塊中定義,numbers
和io
中也有一些。
如下是collections.abc
中16個抽象基類的UML圖(關於多重繼承的內容將在之後的文章中講解):
有幾個抽象基類值得注意:
Iterable
、Container
和Sized
:各個集合類應該繼承這三個抽象基類,或者至少實現兼容的協議。Iterable
經過__iter__
方法支持迭代;Container
經過__contains__
方法支持in
運算;Sized
經過__len__
方法支持len()
函數;Sequence
、Mapping
和Set
:這三個是主要的不可變集合類型,並且各自都有可變的子類,即MutableSequence
、MutableMapping
和MutableSet
。Callable
和Hashable
:從圖上能夠看出,這兩個抽象基類在標準庫中沒有子類。在numbers
包中的抽象基類的繼承關係則很簡單,都是線性的(「數字塔」)。下面5個類從左到右依次派生:
Number
,Complex
,Real
,Rational
,Integral
下面咱們將自行定義一個抽象基類並繼承出它的子類。但這並非鼓勵各位在生產代碼中自定義抽象基類!
咱們將模擬一個隨機抽獎機,它的抽象基類是Tombola
,它的4個方法以下:
.load(...)
:抽象方法,把元素放入容器;.pick()
:抽象方法,從容器中隨機返回一個元素,並從容器中刪除該元素;.loaded()
:當容器不爲空是返回True
;.inspect()
:返回一個有序元組,由容器中的現有元素構成,不修改容器的內容(容器內部元素順序不保留)。它和它的三個子類的UML圖以下:
如下是Tombola
的定義:
# 代碼3.1 import abc class Tombola(abc.ABC): @abc.abstractmethod def load(self, iterable): """從可迭代對象中添加元素""" @abc.abstractmethod def pick(self): """隨機刪除元素,而後將其返回。 若是實例爲空,這個方法應該拋出LookupError, 這個異常是IndexError和KeyError的基類""" def loaded(self): # 比較耗時,子類可重寫 """當容器不爲空時返回True""" 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))
解釋及補充:
super()
函數調用抽象方法,爲它添加功能,而不是從頭開始寫;abc
的模塊,一個是前面說的collections.abc
,另外一個就是這裏的abc
模塊。只有在新定義抽象基類的時候才用獲得abc.ABC
,每一個抽象基類都依賴這個類。在abc
模塊中原本還有@abstractclassmethod
,@abstractstaticmethod
和@abstractproperty
三個裝飾器,但這三個從Python3.3起被廢除了,由於這三個的功能都能在@abstractmethod
上堆疊其餘裝飾器獲得,好比實現@abstractclassmethod
的功能:
# 代碼3.2 class MyABC(abc.ABC): @classmethod @abc.abstractmethod def an_abstract_classmethod(cls, ...): pass
如下是它的兩個子類的實現代碼:
# # 代碼3.3 class BingoCage(Tombola): # loaded()和inspect()延用抽象基類的實現 def __init__(self, items): self._randomizer = random.SystemRandom() # 它會調用os.urandom() self._items = [] self.load(items) # 委託給load()方法實現初始加載 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: # 爲了兼容Tombola,並非拋出ValueError raise LookupError("pick from empty LotteryBlower") return self._balls.pop(position) def loaded(self): # 覆蓋了抽象基類低效的版本 return bool(self._balls) def inspect(self): return tuple(sorted(self._balls))
上面兩個子類都是直接繼承自Tombola
,而白鵝類型有一個基本特性:即使不用繼承,也能將一個類註冊爲抽象基類的虛擬子類。下面是TomboList
的實現:
# 代碼3.4 @Tombola.register # 把TomboList註冊爲Tombola的虛擬子類 class TomboList(list): # 它同時仍是list的真實子類,而list實際上是MutableSequence的虛擬子類 def pick(self): if self: position = random.randrange(len(self)) return self.pop(position) else: raise LookupError("pick from empty LotteryBlower") load = list.extend # 當我看到竟然這麼實現方法時,感受本身好膚淺...... def loaded(self): return bool(self) def inspect(self): return tuple(sorted(self)) # Tombola.register(TomboList) 這是register的函數調用版本
下面是這個子類的簡單使用:
# 代碼3.5 >>> issubclass(TomboList, Tombola) True # TomboList是Tombola的子類 >>> t = TomboList(range(100)) >>> isinstance(t, Tombola) True # TomboList的實例也是Tombola類型 >>> TomboList.__mro__ (<class 'mytest.TomboList'>, <class 'list'>, <class 'object'>) >>> TomboList.__subclasses__() [<class 'mytest.BingoCage'>, <class 'mytest.LotteryBlower'>]
解釋及補充:
__mro__
中,即方法解析順序(Method Resolution Order)。它按順序列出類及其超類,Python則會按照這個順序搜索方法。從上述結果能夠看出,這個屬性只存儲了「真實的」超類。__subclasses__
方法返回類的直接子類列表,不含虛擬子類;register
能夠當作裝飾器用,但更經常使用的作法仍是把它當函數使用。鵝的行爲有可能像鴨子。先看以下代碼:
# 代碼3.6 >>> class Struggle: ... def __len__(self): return 23 ... >>> from collections import abc >>> isinstance(Struggle(), abc.Sized) True >>> issubclass(Struggle, abc.Sized) True
這裏既沒有繼承,也沒有註冊,但Struggle
依然被issubclass
判斷爲abc.Sized
的子類。之因此會這樣,是由於abc.Sized
實現了一個特殊的類方法__subclasshook__
:
# # 代碼3.7,abc.Sized的實如今 _collections_abc.py 中 class Sized(metaclass=ABCMeta): __slots__ = () @abstractmethod def __len__(self): return 0 @classmethod def __subclasshook__(cls, C): if cls is Sized: # 源代碼中是 return _check_methods(C, "__len__"),這裏修改了一下 if any("__len__" in B.__dict__ for B in C.__mro__): return True return NotImplemented
這像不像鴨子類型?只要實現了__len__
方法,這個類就是abc.Sized
的子類。
在自定義的抽象基類中並不必定要實現__subclasshook__
方法,由於即便在Python源碼中,目前也只見到Sized
這一個抽象基類實現了__subclasshook__
方法,並且Sized
只有一個特殊方法。在決定自行實現__subclasshook__
方法以前,請想清楚你必定須要這個方法嗎?你的能力可以保證這個方法的可靠性嗎?
本篇討論的話題只有一個,即「接口」。首先咱們討論了鴨子類型的高度動態性,它實現的是動態協議,也是非正式接口;隨後咱們藉助「白鵝類型」,使用抽象基類明確地、顯示地聲明接口,而後經過子類或註冊來實現這些接口。期間,咱們自定義了一個抽象基類,並經過繼承實現了它的兩個子類,還經過註冊實現了它的一個虛擬子類。
最後,仍是那句話:不要輕易自定義抽象基類,除非你想構件容許用戶擴展的框架。平常使用中,咱們與抽象基類的聯繫應該是建立現有抽象基類的子類,或者使用現有的抽象基類註冊。本身從頭編寫新抽象基類的狀況很是少。
迎你們關注個人微信公衆號"代碼港" & 我的網站 www.vpointer.net ~