使用Python的PEAK來適配協(xié)議的教程
如果您正嘗試去處理元類,或者正受困于 Twisted 中的異步編程,或者正在研究由于使用了多分派而使您精疲力盡的面向?qū)ο缶幊?,那么您完全錯了!PEAK 將所有這些中的一些要素組合到了一個組件編程框架中。PEAK 還存在一些小問題。類似于 Twisted,PEAK 的文檔 -- 盡量數(shù)量巨大 -- 難以看懂。但是盡管如此,關(guān)于 Python 領(lǐng)袖 Phillip J. Eby 領(lǐng)導(dǎo)的這一項目還是有一些東西非常值得關(guān)注;而且,我覺得,有機會進(jìn)行極具生產(chǎn)價值的并且層次特別高的應(yīng)用程序開發(fā)。
PEAK 包由許多不同用途的子包組成。一些重要的子包是 peak.api、 peak.binding、 peak.config、 peak.naming 和 peak.storage 。那些名字大部分是自我解釋性的。子包 peak.binding 用于組件間的靈活連接; peak.config 讓您可以存儲“很少改變的(lazily immutable)”數(shù)據(jù),這些數(shù)據(jù)與聲明性應(yīng)用程序(declarative application )編程有關(guān); peak.naming 讓您可以為(網(wǎng)絡(luò)的)資源創(chuàng)建全局惟一的標(biāo)識符; peak.storage 顧名思義讓您可以管理數(shù)據(jù)庫和持久內(nèi)容。
不過,對本文來說,我們將關(guān)注的是 peak.api 。特別是 PyProtocols 包,它可以單獨獲得并為其他 PEAK 子包提供一個基礎(chǔ)設(shè)施。在 peak.api.protocols 中包括了 PyProtocols 包的一個版本。不過現(xiàn)在我所感興趣的是研究一個獨立的 protocols 包。在以后的部分,我將返回來討論 PEAK 其他部分的話題。
什么是協(xié)議?
抽象地說,協(xié)議只是對象同意遵循的一組行為。強類型(Strongly-typed)編程語言 -- 包括 Python -- 都有一個基本類型的集合,每個基本類型都有一組得到保證的行為:Integer 知道如何去求它們自己的乘積;list 知道如何去遍歷它們的內(nèi)容;dictionary 知道如何根據(jù)一個關(guān)鍵字找到相應(yīng)的值;file 知道如何去讀和寫字節(jié);諸如此類。您可以預(yù)期的內(nèi)置類型的行為集合構(gòu)成了它們實現(xiàn)的一個 協(xié)議。對協(xié)議進(jìn)行系統(tǒng)化的對象被稱為 接口(interface)。
對標(biāo)準(zhǔn)的類型而言,將實現(xiàn)的所有行為全部列出并不太困難(盡管不同的 Python 版本之間會稍有不同;或者,不同的編程語言之間當(dāng)然會有差別)。但是,在邊界 -- 對于屬于自定義類的對象來說 -- 難以聲明最終是什么構(gòu)成了“類-dictionary”或“類-file”的行為。大部分情況下,只實現(xiàn)了比如內(nèi)置的 dict 類型的方法的一個子集 -- 甚至是相當(dāng)小的子集 -- 的自定義對象,就足夠“類-dictionary”而可以滿足當(dāng)前的要求。不過,能顯式地整理出一個對象要用到的函數(shù)、模塊、類或者框架中需要能夠做哪些事情,將是很吸引人的。那就是 PyProtocols 包所做到的(一部分)。
在具有靜態(tài)類型聲明的編程語言中,為了在新的上下文中使用數(shù)據(jù),您通常需要將其自一個類型 強制類型轉(zhuǎn)換(cast)或者 轉(zhuǎn)換(convert)到另一個類型。在其他語言中,轉(zhuǎn)換根據(jù)上下文的需要隱式地進(jìn)行,這些被稱為 強迫同型(coercions)。Python 中既有強制類型轉(zhuǎn)換也有強迫同型,通常使用更多的是前者(“顯式優(yōu)于隱式”)。您可以將向一個浮點數(shù)加到一個整型數(shù),結(jié)果得到一個更為通用的浮點數(shù);但是如果您希望將字符串 "3.14" 轉(zhuǎn)換為一個數(shù)字,那么您需要使用顯式的構(gòu)造函數(shù) float("3.14") 。
PyProtocols 具有一個稱為“適配(adaptation)”的功能,類似于“部分類型(partial typing)”這一非正統(tǒng)計算機科學(xué)概念。適配還可能被認(rèn)為是“加速的強制同型”。如果一個接口定義了所需要的一組能力 (也就是對象方法),那么要去做“所需要的一切”的對象就要求適配 -- 通過 protocols.adapt() 函數(shù)實現(xiàn) -- 以提供所需要的能力。顯然,如果您有一個顯式的轉(zhuǎn)換函數(shù)可以將類型 X 的對象轉(zhuǎn)換為類型 Y 的對象(在這里 Y 實現(xiàn)了某個 IY 接口),那么那個函數(shù)要能夠讓 X 適配協(xié)議 IY 。不過,PyProtocols 中的適配可以做比這多得多的事情。例如,甚至如果您從來沒有顯式地編寫過從類型 X 到類型 Y 的轉(zhuǎn)換程序, adapt() 通??梢酝蒲莩鲆粭l讓 X 提供 IY 所要求的能力的途徑(也就是說,找到從 X 到接口 IZ ,從 IZ 到 IW ,然后再從 IW 到 IY 的中間轉(zhuǎn)換)。
聲明接口和適配器
在 PyProtocols 中有很多不同的方法可以創(chuàng)建接口和適配器。PyProtocols 文檔非常詳細(xì)地介紹了這些技術(shù) -- 很多不會在本文中涉及。接下來我們將進(jìn)入一些細(xì)節(jié),不過,我覺得,在這里給出實際的 PyProtocols 代碼的一個最簡化實例是個有用的方法。
例如,我決定創(chuàng)建一個 Python 對象的類-Lisp 序列化。其描述并不是準(zhǔn)確的 Lisp 語法,我也并不在意這種格式確切的優(yōu)點和缺點。在這里,我的想法只是創(chuàng)建一個功能,使之可以執(zhí)行類似 repr() 函數(shù)或 pprint 模塊的工作,不過結(jié)果是既與以前串行器(serializers)有明顯的不同,又要能更容易地擴展/定制。出于舉例說明的目的做出了一個非常不像 Lisp 的選擇:映射(mappings)是一個比列表(list)更為基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu)(Python 的元組(tuple)或列表被作為以連續(xù)整數(shù)為鍵的映射來處理)。下面是代碼:
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)用一些不同的方法聲明了許多適配器。在一些情況中,代碼將一個接口轉(zhuǎn)換到另一個接口;在其他情況中,類型本身直接適配到另一個接口。我希望您能注意到關(guān)于代碼的一些方面:(1)沒有創(chuàng)建任何從 list 或 tuple 到 ILisp 接口的適配器;(2)沒有為 int 數(shù)字類型顯式聲明適配器;(3)就此而言,沒有聲明直接由 dict 到 ILisp 的適配器。下面是代碼將如何適配( adapt() )各種 Python 對象:
test_lispy.py 對象序列化
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')
運行時,我們得到:
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)
對我們的輸出進(jìn)行一些解釋將會有所幫助。第一行比較簡單,我們定義了一個直接從字符串到 ILisp 的適配器,對 adapt("foo bar", ILisp) 的調(diào)用只是返回了 lambda 函數(shù)的結(jié)果。下一行只是有一點復(fù)雜。沒有直接從 dict 到 ILisp 的適配器;但我們不必使用任何適配器就可以讓 dict 去適配 IMap (我們聲明了足夠多),而且我們有從 IMap 到 ILisp 的適配器。類似的,對于后面的列表和元組,我們可以使 ILisp 適配 ISeq ,使 ISeq 適配 IMap ,并使 IMap 適配 ILisp 。PyProtocols 會指出要采取的適配路徑,所有這些不可思議的過程都在幕后完成。一個舊風(fēng)格的實例所經(jīng)歷的過程與字符串或者支持 IMap 的對象相同,我們有一個直接到 ILisp 的適配。
不過,等一下。在我們的 dict 和 tuple 對象中用到的所有的整數(shù)是怎么處理的呢? long 、 complex、float 和 bool 類型的數(shù)字有顯式的適配器,不過 int 一個都沒有。這里的技巧在于, int 對象已經(jīng)擁有一個 .__repr__() 方法;通過將隱式支持聲明為 ILisp 接口的一部分,我們可以巧妙地使用對象已有的 .__repr__() 方法作為對 ILisp 接口的支持。實際上,作為一個內(nèi)置的類型,整數(shù)用不加任何修飾的阿拉伯?dāng)?shù)字表示,而不使用大寫的類型初始器(比如 LONG )。
適配協(xié)議
讓我們來更明確地看一下 protocol.adapt() 函數(shù)都做了什么事情。在我們的例子中,我們使用“聲明 API(declaration API)”來隱式地為適配設(shè)置了一組“工廠(factories)”。這個 API 有幾個層次。聲明 API 的“基本層次(primitives)”是函數(shù): declareAdaptorForType() 、 declareAdaptorForObject() 和 declareAdaptorForProtocol() 。前面的例子中沒有用到這些,而是用到了一些高層次的 API,如 declareImplementation() 、 declareAdaptor() 、 adviceObject() 和 protocolForType() 。在一種情況下,我們看到在一個類體中有“奇妙”的 advise() 聲明。 advise() 函數(shù)支持用于配置那些建議的類的目的和角色的大量關(guān)鍵字參數(shù)。您還可以建議 (advise()) 一個模塊對象。
您不需要使用聲明 API 來創(chuàng)建知道如何使對象適配( adapt() )自己的可適配的對象或者接口。讓我們來看 adapt() 的調(diào)用標(biāo)記,然后解釋它隨后的過程。對 adapt() 的調(diào)用類似這樣:
adapt() 的調(diào)用標(biāo)記
adapt(component, protocol, [, default [, factory]])
這就表示您希望讓對象 component 去適配接口 protocol 。如果指定了 default ,它可以返回為一個包裝對象(wrapper object)或者對 component 的修改。如果 factory 被指定為一個關(guān)鍵字參數(shù),那么會使用一個轉(zhuǎn)換工廠來生成包裝或者修改。不過讓我們先退回一點,來看一下 adapt() 嘗試的完整的動作次序(簡化的代碼):
adapt() 的假想實現(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
對 adapt()的調(diào)用 應(yīng)該保持一些特性(不過這是對程序員的建議,而不是庫的一般強制要求)。對 adapt() 的調(diào)用應(yīng)該是等冪的。也就是說,對于一個對象 x 和一個協(xié)議 P ,我們希望: adapt(x,P)==adapt(adapt(x,P),P) 。高級地,這樣做的目的類似于從 .__iter__() 方法返回自身( self )的迭代器(iterator)類的目的。您基本上不會希望去重新適配到您已經(jīng)適配到的相同類型以產(chǎn)生波動的結(jié)果。
還值得注意的是,適配可能是有損耗的。為了讓一個對象去順應(yīng)一個接口,可能不方便或者不可能保持重新初始化這個對象所需要的所有信息。也就是說,通常情況下,對對象 x 及協(xié)議 P1 和 P2 而言: adapt(x,P1)!=adapt(adapt(adapt(x,P1),P2),P1) 。
在結(jié)束之前,讓我們來看另一個利用了 adapt() 的低層次行為的測試腳本:
test_lispy2.py 對象序列化
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è)計不能滿足等冪的目標(biāo)。改進(jìn)這一設(shè)計可能是個不錯的練習(xí)。不過,像 ILisp 這樣的描述肯定會損耗原始對象中的信息(這是沒關(guān)系的)。
結(jié)束語
感覺上,PyProtocols 與本專欄提及的其他“外來”話題有一些共同之處。首先,聲明 API 是聲明性的(相對于解釋性)。聲明性編程并不給出執(zhí)行一個動作所需要的步驟和開關(guān),而是聲明處理特定的內(nèi)容,由庫或編譯器來具體指出如何執(zhí)行。名稱“declare*()”和“advice*()”正在來自于這一觀點。
不過,我也發(fā)現(xiàn) PyProtocols 編程有些類似于使用多分派進(jìn)行編程,具體說就是使用我在另一期文章提到的 gnosis.magic.multimethods 模塊。與 PyProtocols 的確定適配路徑形成對照,我自己的模塊執(zhí)行了一個相對簡單的推演,確定要分派的相關(guān)祖先類。不過兩個庫都傾向于在編程中鼓勵使用類似的模塊化思想 -- 由大量的小函數(shù)或類來執(zhí)行“可插入的”任務(wù),不需要受死板的類層級結(jié)構(gòu)所困。在我看來,這種風(fēng)格有其優(yōu)越之處。
相關(guān)文章
Python中cv2.Canny() 函數(shù)使用方法
cv2.Canny() 函數(shù)是 OpenCV 中的邊緣檢測函數(shù)之一,用于檢測圖像的邊緣,它的基本原理是通過計算圖像中每個像素點的梯度值來檢測邊緣,本文通過示例代碼介紹Python中cv2.Canny() 函數(shù)用法,需要的朋友參考下吧2023-07-07Python 16進(jìn)制與中文相互轉(zhuǎn)換的實現(xiàn)方法
今天小編就為大家分享一篇Python 16進(jìn)制與中文相互轉(zhuǎn)換的實現(xiàn)方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-07-07