使用Python的PEAK來(lái)適配協(xié)議的教程
如果您正嘗試去處理元類(lèi),或者正受困于 Twisted 中的異步編程,或者正在研究由于使用了多分派而使您精疲力盡的面向?qū)ο缶幊蹋敲茨耆e(cuò)了!PEAK 將所有這些中的一些要素組合到了一個(gè)組件編程框架中。PEAK 還存在一些小問(wèn)題。類(lèi)似于 Twisted,PEAK 的文檔 -- 盡量數(shù)量巨大 -- 難以看懂。但是盡管如此,關(guān)于 Python 領(lǐng)袖 Phillip J. Eby 領(lǐng)導(dǎo)的這一項(xiàng)目還是有一些東西非常值得關(guān)注;而且,我覺(jué)得,有機(jī)會(huì)進(jìn)行極具生產(chǎn)價(jià)值的并且層次特別高的應(yīng)用程序開(kāi)發(fā)。
PEAK 包由許多不同用途的子包組成。一些重要的子包是 peak.api、 peak.binding、 peak.config、 peak.naming 和 peak.storage 。那些名字大部分是自我解釋性的。子包 peak.binding 用于組件間的靈活連接; peak.config 讓您可以存儲(chǔ)“很少改變的(lazily immutable)”數(shù)據(jù),這些數(shù)據(jù)與聲明性應(yīng)用程序(declarative application )編程有關(guān); peak.naming 讓您可以為(網(wǎng)絡(luò)的)資源創(chuàng)建全局惟一的標(biāo)識(shí)符; peak.storage 顧名思義讓您可以管理數(shù)據(jù)庫(kù)和持久內(nèi)容。
不過(guò),對(duì)本文來(lái)說(shuō),我們將關(guān)注的是 peak.api 。特別是 PyProtocols 包,它可以單獨(dú)獲得并為其他 PEAK 子包提供一個(gè)基礎(chǔ)設(shè)施。在 peak.api.protocols 中包括了 PyProtocols 包的一個(gè)版本。不過(guò)現(xiàn)在我所感興趣的是研究一個(gè)獨(dú)立的 protocols 包。在以后的部分,我將返回來(lái)討論 PEAK 其他部分的話題。
什么是協(xié)議?
抽象地說(shuō),協(xié)議只是對(duì)象同意遵循的一組行為。強(qiáng)類(lèi)型(Strongly-typed)編程語(yǔ)言 -- 包括 Python -- 都有一個(gè)基本類(lèi)型的集合,每個(gè)基本類(lèi)型都有一組得到保證的行為:Integer 知道如何去求它們自己的乘積;list 知道如何去遍歷它們的內(nèi)容;dictionary 知道如何根據(jù)一個(gè)關(guān)鍵字找到相應(yīng)的值;file 知道如何去讀和寫(xiě)字節(jié);諸如此類(lèi)。您可以預(yù)期的內(nèi)置類(lèi)型的行為集合構(gòu)成了它們實(shí)現(xiàn)的一個(gè) 協(xié)議。對(duì)協(xié)議進(jìn)行系統(tǒng)化的對(duì)象被稱(chēng)為 接口(interface)。
對(duì)標(biāo)準(zhǔn)的類(lèi)型而言,將實(shí)現(xiàn)的所有行為全部列出并不太困難(盡管不同的 Python 版本之間會(huì)稍有不同;或者,不同的編程語(yǔ)言之間當(dāng)然會(huì)有差別)。但是,在邊界 -- 對(duì)于屬于自定義類(lèi)的對(duì)象來(lái)說(shuō) -- 難以聲明最終是什么構(gòu)成了“類(lèi)-dictionary”或“類(lèi)-file”的行為。大部分情況下,只實(shí)現(xiàn)了比如內(nèi)置的 dict 類(lèi)型的方法的一個(gè)子集 -- 甚至是相當(dāng)小的子集 -- 的自定義對(duì)象,就足夠“類(lèi)-dictionary”而可以滿足當(dāng)前的要求。不過(guò),能顯式地整理出一個(gè)對(duì)象要用到的函數(shù)、模塊、類(lèi)或者框架中需要能夠做哪些事情,將是很吸引人的。那就是 PyProtocols 包所做到的(一部分)。
在具有靜態(tài)類(lèi)型聲明的編程語(yǔ)言中,為了在新的上下文中使用數(shù)據(jù),您通常需要將其自一個(gè)類(lèi)型 強(qiáng)制類(lèi)型轉(zhuǎn)換(cast)或者 轉(zhuǎn)換(convert)到另一個(gè)類(lèi)型。在其他語(yǔ)言中,轉(zhuǎn)換根據(jù)上下文的需要隱式地進(jìn)行,這些被稱(chēng)為 強(qiáng)迫同型(coercions)。Python 中既有強(qiáng)制類(lèi)型轉(zhuǎn)換也有強(qiáng)迫同型,通常使用更多的是前者(“顯式優(yōu)于隱式”)。您可以將向一個(gè)浮點(diǎn)數(shù)加到一個(gè)整型數(shù),結(jié)果得到一個(gè)更為通用的浮點(diǎn)數(shù);但是如果您希望將字符串 "3.14" 轉(zhuǎn)換為一個(gè)數(shù)字,那么您需要使用顯式的構(gòu)造函數(shù) float("3.14") 。
PyProtocols 具有一個(gè)稱(chēng)為“適配(adaptation)”的功能,類(lèi)似于“部分類(lèi)型(partial typing)”這一非正統(tǒng)計(jì)算機(jī)科學(xué)概念。適配還可能被認(rèn)為是“加速的強(qiáng)制同型”。如果一個(gè)接口定義了所需要的一組能力 (也就是對(duì)象方法),那么要去做“所需要的一切”的對(duì)象就要求適配 -- 通過(guò) protocols.adapt() 函數(shù)實(shí)現(xiàn) -- 以提供所需要的能力。顯然,如果您有一個(gè)顯式的轉(zhuǎn)換函數(shù)可以將類(lèi)型 X 的對(duì)象轉(zhuǎn)換為類(lèi)型 Y 的對(duì)象(在這里 Y 實(shí)現(xiàn)了某個(gè) IY 接口),那么那個(gè)函數(shù)要能夠讓 X 適配協(xié)議 IY 。不過(guò),PyProtocols 中的適配可以做比這多得多的事情。例如,甚至如果您從來(lái)沒(méi)有顯式地編寫(xiě)過(guò)從類(lèi)型 X 到類(lèi)型 Y 的轉(zhuǎn)換程序, adapt() 通??梢酝蒲莩鲆粭l讓 X 提供 IY 所要求的能力的途徑(也就是說(shuō),找到從 X 到接口 IZ ,從 IZ 到 IW ,然后再?gòu)?IW 到 IY 的中間轉(zhuǎn)換)。
聲明接口和適配器
在 PyProtocols 中有很多不同的方法可以創(chuàng)建接口和適配器。PyProtocols 文檔非常詳細(xì)地介紹了這些技術(shù) -- 很多不會(huì)在本文中涉及。接下來(lái)我們將進(jìn)入一些細(xì)節(jié),不過(guò),我覺(jué)得,在這里給出實(shí)際的 PyProtocols 代碼的一個(gè)最簡(jiǎn)化實(shí)例是個(gè)有用的方法。
例如,我決定創(chuàng)建一個(gè) Python 對(duì)象的類(lèi)-Lisp 序列化。其描述并不是準(zhǔn)確的 Lisp 語(yǔ)法,我也并不在意這種格式確切的優(yōu)點(diǎn)和缺點(diǎn)。在這里,我的想法只是創(chuàng)建一個(gè)功能,使之可以執(zhí)行類(lèi)似 repr() 函數(shù)或 pprint 模塊的工作,不過(guò)結(jié)果是既與以前串行器(serializers)有明顯的不同,又要能更容易地?cái)U(kuò)展/定制。出于舉例說(shuō)明的目的做出了一個(gè)非常不像 Lisp 的選擇:映射(mappings)是一個(gè)比列表(list)更為基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu)(Python 的元組(tuple)或列表被作為以連續(xù)整數(shù)為鍵的映射來(lái)處理)。下面是代碼:
lispy.py PyProtocol 定義
from protocols import * from cStringIO import StringIO # Like unicode, & even support objects that don't explicitly support ILisp ILisp = protocolForType(unicode, ['__repr__'], implicit=True) # Class for interface, but no methods specifically required class ISeq(Interface): pass # Class for interface, extremely simple mapping interface class IMap(Interface): def items(): "A requirement for a map is to have an .items() method" # Define function to create an Lisp like representation of a mapping def map2Lisp(map_, prot): out = StringIO() for k,v in map_.items(): out.write("(%s %s) " % (adapt(k,prot), adapt(v,prot))) return "(MAP %s)" % out.getvalue() # Use this func to convert an IMap-supporting obj to ILisp-supporting obj declareAdapter(map2Lisp, provides=[ILisp], forProtocols=[IMap]) # Note that a dict implements an IMap interface with no conversion needed declareAdapter(NO_ADAPTER_NEEDED, provides=[IMap], forTypes=[dict]) # Define and use func to adapt an InstanceType obj to the ILisp interface from types import InstanceType def inst2Lisp(o, p): return "(CLASS '(%s) %s)" % (o.__class__.__name__, adapt(o.__dict__,p)) declareAdapter(inst2Lisp, provides=[ILisp], forTypes=[InstanceType]) # Define a class to adapt an ISeq-supporting obj to an IMap-supporting obj class SeqAsMap(object): advise(instancesProvide=[IMap], asAdapterForProtocols=[ISeq] ) def __init__(self, seq, prot): self.seq = seq self.prot = prot def items(self): # Implement the IMap required .items() method return enumerate(self.seq) # Note that list, tuple implement an ISeq interface w/o conversion needed declareAdapter(NO_ADAPTER_NEEDED, provides=[ISeq], forTypes=[list, tuple]) # Define a lambda func to adapt str, unicode to ILisp interface declareAdapter(lambda s,p: "'(%s)" % s, provides=[ILisp], forTypes=[str,unicode]) # Define a class to adapt several numeric types to ILisp interface # Return a string (ILisp-supporting) directly from instance constructor class NumberAsLisp(object): advise(instancesProvide=[ILisp], asAdapterForTypes=[long, float, complex, bool] ) def __new__(klass, val, proto): return "(%s %s)" % (val.__class__.__name__.upper(), val)
在上面的代碼中,我已經(jīng)用一些不同的方法聲明了許多適配器。在一些情況中,代碼將一個(gè)接口轉(zhuǎn)換到另一個(gè)接口;在其他情況中,類(lèi)型本身直接適配到另一個(gè)接口。我希望您能注意到關(guān)于代碼的一些方面:(1)沒(méi)有創(chuàng)建任何從 list 或 tuple 到 ILisp 接口的適配器;(2)沒(méi)有為 int 數(shù)字類(lèi)型顯式聲明適配器;(3)就此而言,沒(méi)有聲明直接由 dict 到 ILisp 的適配器。下面是代碼將如何適配( adapt() )各種 Python 對(duì)象:
test_lispy.py 對(duì)象序列化
from lispy import * from sys import stdout, stderr toLisp = lambda o: adapt(o, ILisp) class Foo: def __init__(self): self.a, self.b, self.c = 'a','b','c' tests = [ "foo bar", {17:2, 33:4, 'biz':'baz'}, ["bar", ('f','o','o')], 1.23, (1L, 2, 3, 4+4j), Foo(), True, ] for test in tests: stdout.write(toLisp(test)+'\n')
運(yùn)行時(shí),我們得到:
test_lispy.py 序列化結(jié)果
$ python2.3 test_lispy.py '(foo bar) (MAP (17 2) ('(biz) '(baz)) (33 4) ) (MAP (0 '(bar)) (1 (MAP (0 '(f)) (1 '(o)) (2 '(o)) )) ) (FLOAT 1.23) (MAP (0 (LONG 1)) (1 2) (2 3) (3 (COMPLEX (4+4j))) ) (CLASS '(Foo) (MAP ('(a) '(a)) ('(c) '(c)) ('(b) '(b)) )) (BOOL True)
對(duì)我們的輸出進(jìn)行一些解釋將會(huì)有所幫助。第一行比較簡(jiǎn)單,我們定義了一個(gè)直接從字符串到 ILisp 的適配器,對(duì) adapt("foo bar", ILisp) 的調(diào)用只是返回了 lambda 函數(shù)的結(jié)果。下一行只是有一點(diǎn)復(fù)雜。沒(méi)有直接從 dict 到 ILisp 的適配器;但我們不必使用任何適配器就可以讓 dict 去適配 IMap (我們聲明了足夠多),而且我們有從 IMap 到 ILisp 的適配器。類(lèi)似的,對(duì)于后面的列表和元組,我們可以使 ILisp 適配 ISeq ,使 ISeq 適配 IMap ,并使 IMap 適配 ILisp 。PyProtocols 會(huì)指出要采取的適配路徑,所有這些不可思議的過(guò)程都在幕后完成。一個(gè)舊風(fēng)格的實(shí)例所經(jīng)歷的過(guò)程與字符串或者支持 IMap 的對(duì)象相同,我們有一個(gè)直接到 ILisp 的適配。
不過(guò),等一下。在我們的 dict 和 tuple 對(duì)象中用到的所有的整數(shù)是怎么處理的呢? long 、 complex、float 和 bool 類(lèi)型的數(shù)字有顯式的適配器,不過(guò) int 一個(gè)都沒(méi)有。這里的技巧在于, int 對(duì)象已經(jīng)擁有一個(gè) .__repr__() 方法;通過(guò)將隱式支持聲明為 ILisp 接口的一部分,我們可以巧妙地使用對(duì)象已有的 .__repr__() 方法作為對(duì) ILisp 接口的支持。實(shí)際上,作為一個(gè)內(nèi)置的類(lèi)型,整數(shù)用不加任何修飾的阿拉伯?dāng)?shù)字表示,而不使用大寫(xiě)的類(lèi)型初始器(比如 LONG )。
適配協(xié)議
讓我們來(lái)更明確地看一下 protocol.adapt() 函數(shù)都做了什么事情。在我們的例子中,我們使用“聲明 API(declaration API)”來(lái)隱式地為適配設(shè)置了一組“工廠(factories)”。這個(gè) API 有幾個(gè)層次。聲明 API 的“基本層次(primitives)”是函數(shù): declareAdaptorForType() 、 declareAdaptorForObject() 和 declareAdaptorForProtocol() 。前面的例子中沒(méi)有用到這些,而是用到了一些高層次的 API,如 declareImplementation() 、 declareAdaptor() 、 adviceObject() 和 protocolForType() 。在一種情況下,我們看到在一個(gè)類(lèi)體中有“奇妙”的 advise() 聲明。 advise() 函數(shù)支持用于配置那些建議的類(lèi)的目的和角色的大量關(guān)鍵字參數(shù)。您還可以建議 (advise()) 一個(gè)模塊對(duì)象。
您不需要使用聲明 API 來(lái)創(chuàng)建知道如何使對(duì)象適配( adapt() )自己的可適配的對(duì)象或者接口。讓我們來(lái)看 adapt() 的調(diào)用標(biāo)記,然后解釋它隨后的過(guò)程。對(duì) adapt() 的調(diào)用類(lèi)似這樣:
adapt() 的調(diào)用標(biāo)記
adapt(component, protocol, [, default [, factory]])
這就表示您希望讓對(duì)象 component 去適配接口 protocol 。如果指定了 default ,它可以返回為一個(gè)包裝對(duì)象(wrapper object)或者對(duì) component 的修改。如果 factory 被指定為一個(gè)關(guān)鍵字參數(shù),那么會(huì)使用一個(gè)轉(zhuǎn)換工廠來(lái)生成包裝或者修改。不過(guò)讓我們先退回一點(diǎn),來(lái)看一下 adapt() 嘗試的完整的動(dòng)作次序(簡(jiǎn)化的代碼):
adapt() 的假想實(shí)現(xiàn)
if isinstance(component, protocol): return component elif hasattr(component,'__conform__'): return component.__conform__(protocol) elif hasattr(protocol,'__adapt__'): return protocol.__adapt__(component) elif default is not None: return default elif factory is not None: return factory(component, protocol) else: NotImplementedError
對(duì) adapt()的調(diào)用 應(yīng)該保持一些特性(不過(guò)這是對(duì)程序員的建議,而不是庫(kù)的一般強(qiáng)制要求)。對(duì) adapt() 的調(diào)用應(yīng)該是等冪的。也就是說(shuō),對(duì)于一個(gè)對(duì)象 x 和一個(gè)協(xié)議 P ,我們希望: adapt(x,P)==adapt(adapt(x,P),P) 。高級(jí)地,這樣做的目的類(lèi)似于從 .__iter__() 方法返回自身( self )的迭代器(iterator)類(lèi)的目的。您基本上不會(huì)希望去重新適配到您已經(jīng)適配到的相同類(lèi)型以產(chǎn)生波動(dòng)的結(jié)果。
還值得注意的是,適配可能是有損耗的。為了讓一個(gè)對(duì)象去順應(yīng)一個(gè)接口,可能不方便或者不可能保持重新初始化這個(gè)對(duì)象所需要的所有信息。也就是說(shuō),通常情況下,對(duì)對(duì)象 x 及協(xié)議 P1 和 P2 而言: adapt(x,P1)!=adapt(adapt(adapt(x,P1),P2),P1) 。
在結(jié)束之前,讓我們來(lái)看另一個(gè)利用了 adapt() 的低層次行為的測(cè)試腳本:
test_lispy2.py 對(duì)象序列化
from lispy import * class Bar(object): pass class Baz(Bar): def __repr__(self): return "Represent a "+self.__class__.__name__+" object!" class Bat(Baz): def __conform__(self, prot): return "Adapt "+self.__class__.__name__+" to "+repr(prot)+"!" print adapt(Bar(), ILisp) print adapt(Baz(), ILisp) print adapt(Bat(), ILisp) print adapt(adapt(Bat(), ILisp), ILisp) $ python2.3 test_lispy2.py <__main__.Bar object at 0x65250> Represent a Baz object! Adapt Bat to WeakSubset(<type 'unicode'>,('__repr__',))! '(Adapt Bat to WeakSubset(<type 'unicode'>,('__repr__',))!)
結(jié)果證明 lispy.py 的設(shè)計(jì)不能滿足等冪的目標(biāo)。改進(jìn)這一設(shè)計(jì)可能是個(gè)不錯(cuò)的練習(xí)。不過(guò),像 ILisp 這樣的描述肯定會(huì)損耗原始對(duì)象中的信息(這是沒(méi)關(guān)系的)。
結(jié)束語(yǔ)
感覺(jué)上,PyProtocols 與本專(zhuān)欄提及的其他“外來(lái)”話題有一些共同之處。首先,聲明 API 是聲明性的(相對(duì)于解釋性)。聲明性編程并不給出執(zhí)行一個(gè)動(dòng)作所需要的步驟和開(kāi)關(guān),而是聲明處理特定的內(nèi)容,由庫(kù)或編譯器來(lái)具體指出如何執(zhí)行。名稱(chēng)“declare*()”和“advice*()”正在來(lái)自于這一觀點(diǎn)。
不過(guò),我也發(fā)現(xiàn) PyProtocols 編程有些類(lèi)似于使用多分派進(jìn)行編程,具體說(shuō)就是使用我在另一期文章提到的 gnosis.magic.multimethods 模塊。與 PyProtocols 的確定適配路徑形成對(duì)照,我自己的模塊執(zhí)行了一個(gè)相對(duì)簡(jiǎn)單的推演,確定要分派的相關(guān)祖先類(lèi)。不過(guò)兩個(gè)庫(kù)都傾向于在編程中鼓勵(lì)使用類(lèi)似的模塊化思想 -- 由大量的小函數(shù)或類(lèi)來(lái)執(zhí)行“可插入的”任務(wù),不需要受死板的類(lèi)層級(jí)結(jié)構(gòu)所困。在我看來(lái),這種風(fēng)格有其優(yōu)越之處。
相關(guān)文章
python訪問(wèn)抓取網(wǎng)頁(yè)常用命令總結(jié)
這篇文章主要介紹了python訪問(wèn)抓取網(wǎng)頁(yè)常用命令的相關(guān)資料,需要的朋友可以參考下2017-04-04Python中cv2.Canny() 函數(shù)使用方法
cv2.Canny() 函數(shù)是 OpenCV 中的邊緣檢測(cè)函數(shù)之一,用于檢測(cè)圖像的邊緣,它的基本原理是通過(guò)計(jì)算圖像中每個(gè)像素點(diǎn)的梯度值來(lái)檢測(cè)邊緣,本文通過(guò)示例代碼介紹Python中cv2.Canny() 函數(shù)用法,需要的朋友參考下吧2023-07-07深入理解Python虛擬機(jī)中魔術(shù)方法的使用
這篇文章主要給大家介紹在?cpython?當(dāng)中一些比較花里胡哨的魔術(shù)方法,以幫助我們自己實(shí)現(xiàn)比較花哨的功能,當(dāng)然這其中也包含一些也非常實(shí)用的魔術(shù)方法,需要的可以參考下2023-05-05Django Channel實(shí)時(shí)推送與聊天的示例代碼
這篇文章主要介紹了Django Channel實(shí)時(shí)推送與聊天的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04Python 16進(jìn)制與中文相互轉(zhuǎn)換的實(shí)現(xiàn)方法
今天小編就為大家分享一篇Python 16進(jìn)制與中文相互轉(zhuǎn)換的實(shí)現(xiàn)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-07-07