摘要:本篇內(nèi)容將從鴨子類型的動(dòng)態(tài)協(xié)議,逐漸過(guò)渡到使接口更明確能驗(yàn)證實(shí)現(xiàn)是否符合規(guī)定的抽象基類。抽象基類介紹完動(dòng)態(tài)實(shí)現(xiàn)接口后,現(xiàn)在開(kāi)始討論抽象基類,它屬于靜態(tài)顯示地實(shí)現(xiàn)接口。標(biāo)準(zhǔn)庫(kù)中的抽象基類從開(kāi)始,標(biāo)準(zhǔn)庫(kù)提供了抽象基類。
《流暢的Python》筆記。1. 前言本篇是“面向?qū)ο髴T用方法”的第四篇,主要討論接口。本篇內(nèi)容將從鴨子類型的動(dòng)態(tài)協(xié)議,逐漸過(guò)渡到使接口更明確、能驗(yàn)證實(shí)現(xiàn)是否符合規(guī)定的抽象基類(Abstract Base Class, ABC)。
本篇討論P(yáng)ython中接口的實(shí)現(xiàn)問(wèn)題,主要內(nèi)容如下:
補(bǔ)充用鴨子協(xié)議實(shí)現(xiàn)部分接口的一種重要方法:猴子補(bǔ)丁;
說(shuō)明抽象基類的常見(jiàn)用途,即,實(shí)現(xiàn)接口時(shí)作為超類使用;
說(shuō)明抽象基類如何檢查具體子類是否符合接口定義,以及如何使用注冊(cè)機(jī)制聲明一個(gè)類實(shí)現(xiàn)了某個(gè)接口;
說(shuō)明如何不通過(guò)子類化或注冊(cè),也能讓抽象基類自動(dòng)“識(shí)別”任何符合接口的類。
補(bǔ)充在正文之前:
在Python中,“X類對(duì)象”,“X協(xié)議”和“X接口”都是一個(gè)意思。并且,除了抽象基類,類實(shí)現(xiàn)或繼承的公開(kāi)屬性(方法或數(shù)據(jù)屬性),包括特殊方法,都可以看做接口。
關(guān)于接口,還有一個(gè)很實(shí)用的補(bǔ)充定義:對(duì)象公開(kāi)方法的子集,讓對(duì)象在系統(tǒng)中扮演特定的角色。
2. 猴子補(bǔ)丁猴子補(bǔ)丁并不是Python特有,它指動(dòng)態(tài)語(yǔ)言中,不用修改源代碼,在運(yùn)行時(shí)就能對(duì)代碼的功能進(jìn)行動(dòng)態(tài)的追加或變更。下面的代碼展示了猴子補(bǔ)丁的用法:
# 代碼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] # 下面的代碼在控制臺(tái)運(yùn)行 >>> 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]
解釋:
Python中,交互式控制臺(tái)中也支持猴子補(bǔ)丁;
要使用random.shuffle函數(shù),對(duì)象必須實(shí)現(xiàn)__setitem__方法,上述代碼在運(yùn)行時(shí)動(dòng)態(tài)添加所需方法;
猴子補(bǔ)丁很強(qiáng)大,但打補(bǔ)丁的代碼與要打補(bǔ)丁的程序耦合十分緊密,而且往往要處理隱藏的部分(比如“受保護(hù)的”屬性)和沒(méi)有文檔的部分。
上述代碼中set_item函數(shù)的第一個(gè)參數(shù)并不是self,這是想說(shuō)明,每個(gè)Python方法說(shuō)到底都是普通函數(shù),把第一個(gè)參數(shù)命名為self只是一種約定(但別隨意打破這種約定)。
這里之所以講猴子補(bǔ)丁,主要是為了說(shuō)明協(xié)議可以是動(dòng)態(tài)的:即使對(duì)象最初沒(méi)有實(shí)現(xiàn)某個(gè)協(xié)議,當(dāng)需要時(shí),我們也能為它動(dòng)態(tài)添加。
3. 抽象基類介紹完動(dòng)態(tài)實(shí)現(xiàn)接口后,現(xiàn)在開(kāi)始討論抽象基類,它屬于靜態(tài)顯示地實(shí)現(xiàn)接口。
3.1 基本概要說(shuō)明有時(shí)候我們需要明確區(qū)分“抽象類”(并不是指“抽象基類”)與“接口”:以自然界為例,“抽象類”一般用于同一物種同一行為,而“接口”則用于不同物種同一行為。當(dāng)然,這兩個(gè)概念有交叉的部分,某些行為既可以歸到“接口“,也可以歸到”抽象類“,而最后歸到誰(shuí)就見(jiàn)仁見(jiàn)智了。但這兩個(gè)概念又有很大的相似之處,它們的實(shí)質(zhì)都是:讓某些對(duì)象擁有同名的方法或?qū)傩裕唧w實(shí)現(xiàn)不一定相同。
Java更注重這兩者的特性,而Python、C++則更注重這兩者的共性。也因此,Java不支持多重繼承(當(dāng)然,也是為了降低復(fù)雜性),用明確的接口類interface來(lái)區(qū)分與abstract class;而在Python和C++中,則用抽象基類充當(dāng)接口。所以,在Python中,直接繼承自抽象基類,更多表明的是”要實(shí)現(xiàn)某種接口或協(xié)議“,而非”要新建某個(gè)具體類的子類“。
如果要測(cè)試是否繼承自抽象基類,推薦使用isinstance和issubclass方法,而不是is運(yùn)算。但也不要濫用這類方法,因?yàn)檫@種代碼用多了說(shuō)明面向?qū)ο笤O(shè)計(jì)得不好。
說(shuō)道isinstance,還有個(gè)與之相關(guān)的概念,相當(dāng)于“鴨子類型”的強(qiáng)化版:
白鵝類型(goose typing):只要cls是抽象基類,即cls的元素是abc.ABCMeta,就可以使用isinstance(obj, cls)。
小插曲:這是書(shū)中給出的標(biāo)準(zhǔn)定義,筆者讀到這的時(shí)候一臉懵逼。“白鵝類型”是個(gè)名詞,但這定義卻是對(duì)一個(gè)過(guò)程的描述,所以“白鵝類型”到底是個(gè)啥(這到底是翻譯的鍋還是作者的鍋)?后來(lái)谷歌了一下,再自己反復(fù)推敲,得出如下總結(jié):鴨子類型是指某個(gè)實(shí)例實(shí)現(xiàn)了某個(gè)方法,就可以說(shuō)它屬于某個(gè)類型,不一定要繼承;而白鵝類型則是指能被判定成某抽象基類的子類的實(shí)例,即,能使isinstance(obj, cls)返回True的obj就是白鵝類型,其中cls是抽象基類。注意,這些子類并不一定是通過(guò)繼承而來(lái),也可能是通過(guò)注冊(cè)而來(lái),還可能是通過(guò)實(shí)現(xiàn)某些方法而來(lái)。
特別提醒:對(duì)于抽象基類(還有元類)的使用,并不建議在生產(chǎn)代碼中自行定義新的抽象基類和元類。定義抽象基類和元類的工作一般由比較資深的Python程序員來(lái)做,適用于寫(xiě)框架的程序員。而即便是資深Python程序員也不常自己定義抽象基類和元類。
3.2 標(biāo)準(zhǔn)庫(kù)中的抽象基類從Python2.6開(kāi)始,標(biāo)準(zhǔn)庫(kù)提供了抽象基類。大多數(shù)抽象基類在collections.abc模塊中定義,numbers和io中也有一些。
以下是collections.abc中16個(gè)抽象基類的UML圖(關(guān)于多重繼承的內(nèi)容將在以后的文章中講解):
有幾個(gè)抽象基類值得注意:
Iterable、Container和Sized:各個(gè)集合類應(yīng)該繼承這三個(gè)抽象基類,或者至少實(shí)現(xiàn)兼容的協(xié)議。Iterable通過(guò)__iter__方法支持迭代;Container通過(guò)__contains__方法支持in運(yùn)算;Sized通過(guò)__len__方法支持len()函數(shù);
Sequence、Mapping和Set:這三個(gè)是主要的不可變集合類型,而且各自都有可變的子類,即MutableSequence、MutableMapping和MutableSet。
Callable和Hashable:從圖上可以看出,這兩個(gè)抽象基類在標(biāo)準(zhǔn)庫(kù)中沒(méi)有子類。
在numbers包中的抽象基類的繼承關(guān)系則很簡(jiǎn)單,都是線性的(“數(shù)字塔”)。下面5個(gè)類從左到右依次派生:
Number,Complex,Real,Rational,Integral
下面我們將自行定義一個(gè)抽象基類并繼承出它的子類。但這并不是鼓勵(lì)各位在生產(chǎn)代碼中自定義抽象基類!
3.3 自定義抽象基類我們將模擬一個(gè)隨機(jī)抽獎(jiǎng)機(jī),它的抽象基類是Tombola,它的4個(gè)方法如下:
.load(...):抽象方法,把元素放入容器;
.pick():抽象方法,從容器中隨機(jī)返回一個(gè)元素,并從容器中刪除該元素;
.loaded():當(dāng)容器不為空是返回True;
.inspect():返回一個(gè)有序元組,由容器中的現(xiàn)有元素構(gòu)成,不修改容器的內(nèi)容(容器內(nèi)部元素順序不保留)。
它和它的三個(gè)子類的UML圖如下:
以下是Tombola的定義:
# 代碼3.1 import abc class Tombola(abc.ABC): @abc.abstractmethod def load(self, iterable): """從可迭代對(duì)象中添加元素""" @abc.abstractmethod def pick(self): """隨機(jī)刪除元素,然后將其返回。 如果實(shí)例為空,這個(gè)方法應(yīng)該拋出LookupError, 這個(gè)異常是IndexError和KeyError的基類""" def loaded(self): # 比較耗時(shí),子類可重寫(xiě) """當(dāng)容器不為空時(shí)返回True""" return bool(self.inspect()) def inspect(self): # 這只是提供一種實(shí)現(xiàn)方式,子類可覆蓋該方法 """返回一個(gè)有序元組,由當(dāng)前元素構(gòu)成""" items = [] while True: try: # 之所以這么獲取元素,是因?yàn)椴恢雷宇惾绾未鎯?chǔ)元素 items.append(self.pick()) except LookupError: break self.load(items) return tuple(sorted(items))
解釋及補(bǔ)充:
導(dǎo)入時(shí),Python并不會(huì)檢查抽象方法的實(shí)現(xiàn),在運(yùn)行時(shí)才會(huì)真正檢測(cè);
如果子類并沒(méi)有實(shí)現(xiàn)抽象基類中所有的抽象方法,那么這個(gè)子類依然是抽象基類;
抽象方法中可以有實(shí)現(xiàn)代碼。即便實(shí)現(xiàn)了,子類也必須覆蓋抽象方法,但可以使用super()函數(shù)調(diào)用抽象方法,為它添加功能,而不是從頭開(kāi)始寫(xiě);
抽象基類中的具體方法只能依賴抽象基類定義的接口。
標(biāo)準(zhǔn)庫(kù)中有兩個(gè)名為abc的模塊,一個(gè)是前面說(shuō)的collections.abc,另一個(gè)就是這里的abc模塊。只有在新定義抽象基類的時(shí)候才用得到abc.ABC,每個(gè)抽象基類都依賴這個(gè)類。
在abc模塊中本來(lái)還有@abstractclassmethod,@abstractstaticmethod和@abstractproperty三個(gè)裝飾器,但這三個(gè)從Python3.3起被廢除了,因?yàn)檫@三個(gè)的功能都能在@abstractmethod上堆疊其他裝飾器得到,比如實(shí)現(xiàn)@abstractclassmethod的功能:
# 代碼3.2 class MyABC(abc.ABC): @classmethod @abc.abstractmethod def an_abstract_classmethod(cls, ...): pass3.4 定義子類
以下是它的兩個(gè)子類的實(shí)現(xiàn)代碼:
# # 代碼3.3 class BingoCage(Tombola): # loaded()和inspect()延用抽象基類的實(shí)現(xiàn) def __init__(self, items): self._randomizer = random.SystemRandom() # 它會(huì)調(diào)用os.urandom() self._items = [] self.load(items) # 委托給load()方法實(shí)現(xiàn)初始加載 def load(self, items): # 必須實(shí)現(xiàn)抽象方法! self._items.extend(items) self._randomizer.shuffle(self._items) def pick(self): # 必須實(shí)現(xiàn)抽象方法! 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))3.5 虛擬子類
上面兩個(gè)子類都是直接繼承自Tombola,而白鵝類型有一個(gè)基本特性:即便不用繼承,也能將一個(gè)類注冊(cè)為抽象基類的虛擬子類。下面是TomboList的實(shí)現(xiàn):
# 代碼3.4 @Tombola.register # 把TomboList注冊(cè)為Tombola的虛擬子類 class TomboList(list): # 它同時(shí)還是list的真實(shí)子類,而list其實(shí)是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 # 當(dāng)我看到居然這么實(shí)現(xiàn)方法時(shí),感覺(jué)自己好膚淺...... def loaded(self): return bool(self) def inspect(self): return tuple(sorted(self)) # Tombola.register(TomboList) 這是register的函數(shù)調(diào)用版本
下面是這個(gè)子類的簡(jiǎn)單使用:
# 代碼3.5 >>> issubclass(TomboList, Tombola) True # TomboList是Tombola的子類 >>> t = TomboList(range(100)) >>> isinstance(t, Tombola) True # TomboList的實(shí)例也是Tombola類型 >>> TomboList.__mro__ (, , ) >>> TomboList.__subclasses__() [ , ]
解釋及補(bǔ)充:
虛擬子類不會(huì)繼承注冊(cè)的抽象基類,而且任何時(shí)候都不會(huì)檢查它是否符合抽象基類的接口,即便在實(shí)例化時(shí)也不會(huì)檢查(如果你的虛擬子類沒(méi)有實(shí)現(xiàn)抽象方法,在實(shí)例化時(shí)不會(huì)報(bào)錯(cuò),但如果是繼承而來(lái)的話則會(huì)報(bào)錯(cuò)),所以為了避免運(yùn)行時(shí)錯(cuò)誤,虛擬子類應(yīng)該實(shí)現(xiàn)抽象基類的全部方法;
類的繼承關(guān)系存儲(chǔ)在一個(gè)特殊的類屬性__mro__中,即方法解析順序(Method Resolution Order)。它按順序列出類及其超類,Python則會(huì)按照這個(gè)順序搜索方法。從上述結(jié)果可以看出,這個(gè)屬性只存儲(chǔ)了“真實(shí)的”超類。
__subclasses__方法返回類的直接子類列表,不含虛擬子類;
雖然現(xiàn)在register可以當(dāng)做裝飾器用,但更常用的做法還是把它當(dāng)函數(shù)使用。
3.6 另一種虛擬子類鵝的行為有可能像鴨子。先看如下代碼:
# 代碼3.6 >>> class Struggle: ... def __len__(self): return 23 ... >>> from collections import abc >>> isinstance(Struggle(), abc.Sized) True >>> issubclass(Struggle, abc.Sized) True
這里既沒(méi)有繼承,也沒(méi)有注冊(cè),但Struggle依然被issubclass判斷為abc.Sized的子類。之所以會(huì)這樣,是因?yàn)?b>abc.Sized實(shí)現(xiàn)了一個(gè)特殊的類方法__subclasshook__:
# # 代碼3.7,abc.Sized的實(shí)現(xiàn)在 _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
這像不像鴨子類型?只要實(shí)現(xiàn)了__len__方法,這個(gè)類就是abc.Sized的子類。
在自定義的抽象基類中并不一定要實(shí)現(xiàn)__subclasshook__方法,因?yàn)榧词乖赑ython源碼中,目前也只見(jiàn)到Sized這一個(gè)抽象基類實(shí)現(xiàn)了__subclasshook__方法,而且Sized只有一個(gè)特殊方法。在決定自行實(shí)現(xiàn)__subclasshook__方法之前,請(qǐng)想清楚你一定需要這個(gè)方法嗎?你的能力能夠保證這個(gè)方法的可靠性嗎?
4. 總結(jié)本篇討論的話題只有一個(gè),即“接口”。首先我們討論了鴨子類型的高度動(dòng)態(tài)性,它實(shí)現(xiàn)的是動(dòng)態(tài)協(xié)議,也是非正式接口;隨后我們借助“白鵝類型”,使用抽象基類明確地、顯示地聲明接口,然后通過(guò)子類或注冊(cè)來(lái)實(shí)現(xiàn)這些接口。期間,我們自定義了一個(gè)抽象基類,并通過(guò)繼承實(shí)現(xiàn)了它的兩個(gè)子類,還通過(guò)注冊(cè)實(shí)現(xiàn)了它的一個(gè)虛擬子類。
最后,還是那句話:不要輕易自定義抽象基類,除非你想構(gòu)件允許用戶擴(kuò)展的框架。日常使用中,我們與抽象基類的聯(lián)系應(yīng)該是創(chuàng)建現(xiàn)有抽象基類的子類,或者使用現(xiàn)有的抽象基類注冊(cè)。自己從頭編寫(xiě)新抽象基類的情況非常少。
迎大家關(guān)注我的微信公眾號(hào)"代碼港" & 個(gè)人網(wǎng)站 www.vpointer.net ~
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/41920.html
摘要:使用抽象基類顯示表示接口如果類的作用是定義接口,應(yīng)該將其明確定義為抽象基類。此外,抽象基類可以作為其他類的唯一基類,混入類則決不能作為唯一的基類,除非這個(gè)混入類繼承了另一個(gè)更具體的混入這種做法非常少見(jiàn)。 《流暢的Python》筆記本篇是面向?qū)ο髴T用方法的第五篇,我們將繼續(xù)討論繼承,重點(diǎn)說(shuō)明兩個(gè)方面:繼承內(nèi)置類型時(shí)的問(wèn)題以及多重繼承。概念比較多,較為枯燥。 1. 繼承內(nèi)置類型 內(nèi)置類型...
摘要:本文重點(diǎn)協(xié)議是中非正式的接口了解抽象基類的基本概念以及標(biāo)準(zhǔn)庫(kù)中的抽象基類掌握抽象基類的使用方法。三抽象基類的使用通過(guò)繼承聲明抽象基類聲明抽象基類最簡(jiǎn)單的方式是繼承或其他抽象基類注意在之間,繼承抽象基類的語(yǔ)法是。 導(dǎo)語(yǔ):本文章記錄了本人在學(xué)習(xí)Python基礎(chǔ)之面向?qū)ο笃闹攸c(diǎn)知識(shí)及個(gè)人心得,打算入門Python的朋友們可以來(lái)一起學(xué)習(xí)并交流。 本文重點(diǎn): 1、協(xié)議是Python中非正式的接...
摘要:自己定義的抽象基類要繼承。抽象基類可以包含具體方法。這里想表達(dá)的觀點(diǎn)是我們可以偷懶,直接從抽象基類中繼承不是那么理想的具體方法。 抽象基類 抽象基類的常見(jiàn)用途: 實(shí)現(xiàn)接口時(shí)作為超類使用。 然后,說(shuō)明抽象基類如何檢查具體子類是否符合接口定義,以及如何使用注冊(cè)機(jī)制聲明一個(gè)類實(shí)現(xiàn)了某個(gè)接口,而不進(jìn)行子類化操作。 如何讓抽象基類自動(dòng)識(shí)別任何符合接口的類——不進(jìn)行子類化或注冊(cè)。 接口在動(dòng)態(tài)類...
摘要:本篇主要講述中使用函數(shù)來(lái)實(shí)現(xiàn)策略模式和命令模式,最后總結(jié)出這種做法背后的思想。 《流暢的Python》筆記。本篇主要講述Python中使用函數(shù)來(lái)實(shí)現(xiàn)策略模式和命令模式,最后總結(jié)出這種做法背后的思想。 1. 重構(gòu)策略模式 策略模式如果用面向?qū)ο蟮乃枷雭?lái)簡(jiǎn)單解釋的話,其實(shí)就是多態(tài)。父類指向子類,根據(jù)子類對(duì)同一方法的不同重寫(xiě),得到不同結(jié)果。 1.1 經(jīng)典的策略模式 下圖是經(jīng)典的策略模式的U...
摘要:具體方法和上一篇一樣,也是用各個(gè)分量的哈希值進(jìn)行異或運(yùn)算,由于的分量可能很多,這里我們使用函數(shù)來(lái)歸約異或值。每個(gè)分量被映射成了它們的哈希值,這些哈希值再歸約成一個(gè)值這里的傳入了第三個(gè)參數(shù),并且建議最好傳入第三個(gè)參數(shù)。 《流暢的Python》筆記。本篇是面向?qū)ο髴T用方法的第三篇。本篇將以上一篇中的Vector2d為基礎(chǔ),定義多維向量Vector。 1. 前言 自定義Vector類的行為...
閱讀 4093·2021-10-08 10:04
閱讀 3074·2021-08-11 11:20
閱讀 2748·2021-07-25 21:37
閱讀 2695·2019-08-30 12:44
閱讀 2324·2019-08-30 11:12
閱讀 1325·2019-08-26 13:45
閱讀 2374·2019-08-26 11:53
閱讀 3068·2019-08-26 11:32