Python编程:接口
1.Python文化中的接口和協(xié)議
? ? ? ? 在Python中,我們把協(xié)議定義為非正式的接口,是讓Python這種動態(tài)類型語言實現(xiàn)多態(tài)的方式。那么,接口在動態(tài)類型語言中是怎么運作的呢?
? ? ? ? 首先,基本的事實是,Python語言沒有interface關(guān)鍵字,而且,除了抽象基類,每個類都有接口:類實現(xiàn)和繼承的公開屬性,包括特殊方法,如:_getitem_和_add_。按照定義,受保護的屬性和私有屬性不在接口中:即便是受保護的屬性,也只能采用命名約定實現(xiàn),私有屬性可以輕松訪問。
? ? ? ? 其次,不要覺得把公開數(shù)據(jù)屬性放入對象的接口中不妥,因為如果需要總能實現(xiàn)讀值方法和設(shè)值方法,把數(shù)據(jù)屬性變成特性,使用obj.attr句法的客戶代碼不受到影響。
class Vector2d:typecode = 'd'def __init__(self, x, y):self.x = float(x)self.y = float(y)def __iter__(self):return (i for i in (self.x, self.y))class Vector2d:typecode = 'd'def __init__(self, x, y):self.x = float(x)self.y = float(y)@propertydef x(self):return self._x@propertydef y(self):return self._ydef __iter__(self):return (i for i in (self.x, self.y))? ? ? ? 上面的代碼中,第一段中x、y是公開的數(shù)據(jù),而第二段x、y是使用特性實現(xiàn)的,將其變成了只讀特性。這段代碼是我們之前實現(xiàn)過的。
? ? ? ? 其實關(guān)于接口,這里還有一個補充的實用定義:對象公開方法的子集,讓對象在系統(tǒng)中扮演特定的角色。也就是說,接口是實現(xiàn)特定角色的方法集合。像Python文檔中所說的文件類對象和可迭代對象就是這個意思。另外,協(xié)議與繼承也沒有關(guān)系,一個類可能會實現(xiàn)多個接口,從而讓實例扮演多個角色。
? ? ? ? 協(xié)議是接口,但不是正式的,因此協(xié)議不能像正式接口那樣施加限制,一個類可能只實現(xiàn)部分接口,這是允許的。
2.Python喜歡序列
? ? ? ? Python數(shù)據(jù)模型的哲學是盡量支持基本協(xié)議。對于序列來說,即便是最簡單的實現(xiàn),Python也會力求做到最好。
class Foo:def __getitem__(self, pos):return range(0, 30, 10)[pos]f = Foo() print(f[0]) print(20 in f) print(15 in f)? ? ? ? 在上面的實例中,定義了Foo類,它并沒有繼承abc.Sequence,而是只實現(xiàn)了序列協(xié)議的一個基本方法。通過運行代碼可以發(fā)現(xiàn),雖然沒有_iter_方法,Foo實例也是可迭代的對象,因為存在_getitem_方法,Python會調(diào)用它,傳入從零開始的整數(shù)索引,嘗試迭代對象(當然,這是一種后備機制,之前講過)。盡管沒有實現(xiàn)_contains_方法,但是Python足夠智能,等迭代Foo實例,也能運行in運算符。這一切都指向了協(xié)議的重要性。
3.是用猴子補丁在運行時實現(xiàn)協(xié)議
? ? ? ? 我們之前定義過FrenchDeck類有個重大的缺陷,就是無法進行洗牌。我們不能直接使用random.shuffle來進行洗牌,因為我們定義的FrenchDeck類是不可變的序列協(xié)議,要將其變成可變的需要提供_setitem_方法。
from random import shuffledef set_card(deck, poisition, card):deck._card[poisition] = cardFrenchDeck.__setitem__ = set_card() shuffle(deck)? ? ? ? 在這個實例中,我們將定義的函數(shù)賦值給FrenchDeck類的_setitem_屬性,然后就可以用shuffle來進行卡牌的打亂了。
? ? ? ? 這個實例的關(guān)鍵是,set_card函數(shù)要知道deck對象有一個名為_cards的屬性,而且它還是可變序列。然后,我們把set_card函數(shù)賦值給_setitem_這個特殊方法,從而將這個方法依附到FrenchDeck類上。這種技術(shù)叫猴子補丁:在運行時修改類和模塊,而不改動源代碼。但是,打補丁的代碼要與打補丁的程序十分耦合,而且要處理好隱藏和沒有文檔的部分。
4.定義抽象基類的子類
? ? ? ? 在Python中,我們一般是先利用現(xiàn)有的抽象基類,然后在斗膽自己定義。下面我們將French
Deck2聲明為collections.MutableMapping的子類。
import collectionsCard = collections.namedtuple('Card', ['rank', 'suit'])class FrenchDeck2(collections.MutableMapping):ranks = [str(n) for n in range(2, 11)] + list('JQKA')suits = 'spades diamonds clubs hearts'.split()def __init__(self):self._cards = [Card(rank, suit) for suit in self.suits for rank in self.ranks]def __len__(self):return len(self._cards)def __getitem__(self, poistion):return self._cards[poistion]def __setitem__(self, poistion, value):self._cards[poistion] = valuedef __delitem__(self, poistion):del self._cards[poistion]def insert(self, poistion, value):self._cards.insert(poistion, value)? ? ? ? 為了支持洗牌,我們只需要實現(xiàn)_setitem_方法,同時因為是繼承collections.MutableMapping類,就必須實現(xiàn)_delitem_方法,并且還要實現(xiàn)_insert_方法。
????????通常,導入時,Python不會檢查抽象方法的實現(xiàn),在運行實例化時才會真正的檢查。當檢查到?jīng)]有正確實現(xiàn)某個抽象方法時,就會拋TypeError異常。這也就是我們要定義insert等方法的原因。另外,要想實現(xiàn)子類,我們可以覆蓋從抽象基類中繼承的方法,以更高效的方式重新實現(xiàn)。
5.標準庫中的抽象基類
? ? ? ? Python標準庫提供了一些抽象基類。大多數(shù)抽象基類在collections.abc模塊中定義,不過其它地方也會有一些。如:numbers和io包。但是總的來說,collections.abc中抽象基類最常用。
5.1collections.abc模塊中的抽象基類
? ? ? ? 在collections.abc模塊中定義了16個抽象基類,簡要的UML類圖,如下圖所示。
(1)Iterable、Container和Sized
? ? ? ? 各個集合應該繼承這三個抽象基類,或者至少實現(xiàn)兼容的協(xié)議。Iterable通過_iter_方法支持迭代、Container通過_contains_方法支持in運算符、Sized通過_len_方法支持len()函數(shù)。
(2)Sequence、Mappling和Set
? ? ? ? 這三個是主要的不可變集合類型,而且各自都有可變的子類。
(3)MappingView
? ? ? ? 在Python3中,映射方法.items()、.keys()和.values()返回的對象是ItemsView、KeysView、ValuesView的實例。前兩個類還從Set類繼承了豐富的接口。
(4)Callable和Hashable
? ? ? ? 這兩個抽象基類與集合沒有太大關(guān)系,只不過是因為它們重要,才把它們放到該模塊中。這兩個抽象基類的作用是為內(nèi)置函數(shù)isinstance提供支持,以一種安全的方式判斷對象能不能調(diào)用或者散列。
(5)Iterator
? ? ? ? 它是Iterable的子類,后續(xù)我們會講到。
5.2抽象基類的數(shù)字塔
? ? ? ? numbers包定義的是數(shù)字塔(即各個抽象基類的層次結(jié)構(gòu)是線性的)。其排序順序如下。
| Number | 位于頂端的超類 |
| Complex | |
| Real | 可以檢查一個數(shù)是否是浮點數(shù) |
| Rational | |
| Intergral | 可以用來檢查一個數(shù)是否是整數(shù) |
6.定義并使用一個抽象基類
? ? ? ? 為了證明有必要定義抽象基類,我們先要在框架中找到使用它的場景。想象一下下面這個場景:你要在網(wǎng)站和移動應用中隨機顯示廣告,但是要在整個廣告清單輪轉(zhuǎn)一遍之前,不重復顯示廣告。假設(shè)我們構(gòu)建一個廣告管理框架,名為ADAM。它的職責之一是,支持用戶提供隨機挑選的無重復類。下面,我們將定義一個Tombola的類來實現(xiàn)這個功能。
import abcclass Tombola(abc.ABC):@abc.abstractmethoddef load(self, iterable):"""從可迭代對象中添加元素"""@abc.abstractmethoddef pick(self):"""隨機刪除元素,然后將其返回。如果實例為空,這個方法應該拋出LookupError"""def loaded(self):"""如果至少有一個元素,就返回Ture"""return bool(self.inspect())def inspect(self):"""返回有序元組,由當前元素構(gòu)成"""items = []while True:try:items.append(self.pick())except LookupError:breakself.load(items)return tuple(sorted(items))? ? ? ? 自己定義的抽象基類要繼承abc.ABC類。抽象方法使用@abstractmethod裝飾器來標記,而且定義體中通常只有文檔字符串,不定義具體的方法,但是,在有的情況下也需要定義具體方法,這時,抽象基類中的具體方法只能依賴抽象基類定義的接口(即只能使用抽象基類中的其它具體方法、抽象方法或特性)。根據(jù)文檔字符串,如果沒有元素可選,應該拋出LookupError。當我們不知道具體子類如何存儲元素時,我們可以不斷調(diào)用pick()方法,吧Tombola清空,再使用load()把所有元素都加進去。
6.1抽象基類句法詳解
? ? ? ? 聲明抽象基類最簡單的方法是繼承abc.ABC或者其他抽象基類。但是,如果你使用的是舊版本的Python,并且繼承現(xiàn)有的抽象基類不可取時,必須在class語句中使用metaclass=關(guān)鍵字。如下:
class Tombola(metaclass=abc.ABCmeta):6.2定義Tombola抽象基類的子類
? ? ? ? 定義好Tombola抽象基類后,我們會開發(fā)兩個具體的子類,滿足Tombola具體的接口。首先,我們先定義一個名為bingocage的具體子類。
import random from tombola import Tombolaclass 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()? ? ? ? 在該段代碼中,我們首先指定其為Tombola的子類,假設(shè)我們將在線上游戲使用這個。我們先使用random模塊來進行隨機,再使用load()方法來進行初始加載。該類會從Tombola類中繼承l(wèi)oaded和inspect方法,當然,我們也可以進行覆蓋,使用個人能夠高效的代碼。
? ? ? ? 其次,我們定義一個LotteryBlower類。
import random from tombola import Tombolaclass LotteryBlower(Tombola):def __init__(self, iterable):self._balls = list(iterable)def load(self, iterable):self._balls.extend(iterable)def pick(self):try:poistion = random.randrange(len(self._balls))except ValueError:raise LookupError('pick from empty LotteryBlower')return self._balls.pop(poistion)def loaded(self):return bool(self._balls)def inspect(self):return tuple(sorted(self._balls))? ? ? ? 同樣,LotteryBlower類也是為Tombola的子類。該類首先初始化方法接收任何可迭代的對象:把參數(shù)構(gòu)建成列表。隨后,在pick方法中檢查,如果范圍為空,函數(shù)會拋出ValueError,當然,我們?yōu)榱思嫒?#xff0c;拋出LookupError。如果范圍不為空,就從其中隨機彈出元素。最后,通過重新定義loaded和inspect方法,覆蓋了原有的代碼。
6.3Tombola的虛擬子類
? ? ? ? “白鵝”類型的一個基本特性:即便不繼承,也有辦法把一個類注冊為抽象基類的虛擬子類。這樣做時,我們保證注冊的類忠實地實現(xiàn)了抽象基類定義的接口,而Python也會相信我們。一旦我們說謊了,就會拋出異常。
? ? ? ? 注冊虛擬子類的方法是在抽象基類上調(diào)用register方法。這樣做之后,注冊的類就會變成抽象基類的虛擬子類,而且issubclass和isinstance等函數(shù)都能識別。但是,注冊的類不會從抽象基類中繼承任何屬性和方法。
????????register方法通常作為普通函數(shù)來進行使用,但有時也作為裝飾器進行使用。如下:
from random import randrange from tombola import Tombola@Tombola.register class Tombolist(list):def pick(self):if self:poistion = randrange(len(self))return self.pick(poistion)else:raise LookupError('pop from empty TomboList')load = list.extenddef loaded(self):return bool(self)def inspect(self):return tuple(sorted(self))? ? ? ? ?我們先是把Tombolist注冊為Tombola的虛擬子類,又將Tombolist拓展為list,因此,用它繼承l(wèi)ist類中的方法,可以調(diào)用pop等方法。pick方法調(diào)用繼承自list的pop方法,傳入一個隨機元素的索引。? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 7.Tombola子類的測試方法? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ????
????????我們編寫的測試實例中有兩個類的屬性,用它們內(nèi)省類的繼承關(guān)系。
(1)_subclasses_
? ? ? ? 這個方法返回類的直接子類列表,不含虛擬子類。
(2)_abc_registry_
? ? ? ? 只有抽象基類有這個數(shù)據(jù)屬性,其值是一個WeakSet對象,即抽象類注冊的虛擬子類的弱引用。? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
總結(jié)
以上是生活随笔為你收集整理的Python编程:接口的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 利用Arcgis制作2019年重庆主城新
- 下一篇: 海康大华硬件NVR如何配置接入GB/T2