如果您正嘗試去處理元類,或者正受困於 Twisted 中的非同步編程,或者正在研究由於使用了多指派而使您精疲力盡的物件導向編程,那麼您完全錯了!PEAK 將所有這些中的一些要素組合到了一個組件編程架構中。PEAK 還存在一些小問題。類似於 Twisted,PEAK 的文檔 -- 盡量數量巨大 -- 難以看懂。但是儘管如此,關於 Python 領袖 Phillip J. Eby 領導的這一項目還是有一些東西非常值得關注;而且,我覺得,有機會進行極具生產價值的並且層次特別高的應用程式開發。
PEAK 包由許多不同用途的子包組成。一些重要的子包是 peak.api、 peak.binding、 peak.config、 peak.naming 和 peak.storage 。那些名字大部分是自我解釋性的。子包 peak.binding 用於組件間的靈活串連; peak.config 讓您可以儲存“很少改變的(lazily immutable)”資料,這些資料與宣告式應用程式(declarative application )編程有關; peak.naming 讓您可以為(網路的)資源建立全域惟一的標識符; peak.storage 顧名思義讓您可以管理資料庫和持久內容。
不過,對本文來說,我們將關注的是 peak.api 。特別是 PyProtocols 包,它可以單獨獲得並為其他 PEAK 子包提供一個基礎設施。在 peak.api.protocols 中包括了 PyProtocols 包的一個版本。不過現在我所感興趣的是研究一個獨立的 protocols 包。在以後的部分,我將返回來討論 PEAK 其他部分的話題。
什麼是協議?
抽象地說,協議只是對象同意遵循的一組行為。強型別(Strongly-typed)程式設計語言 -- 包括 Python -- 都有一個基本類型的集合,每個基本類型都有一組得到保證的行為:Integer 知道如何去求它們自己的乘積;list 知道如何去遍曆它們的內容;dictionary 知道如何根據一個關鍵字找到相應的值;file 知道如何去讀和寫位元組;諸如此類。您可以預期的內建類型的行為集合構成了它們實現的一個 協議。對協議進行系統化的對象被稱為 介面(interface)。
對標準的類型而言,將實現的所有行為全部列出並不太困難(儘管不同的 Python 版本之間會稍有不同;或者,不同的程式設計語言之間當然會有差別)。但是,在邊界 -- 對於屬於自訂類的對象來說 -- 難以聲明最終是什麼構成了“類-dictionary”或“類-file”的行為。大部分情況下,只實現了比如內建的 dict 類型的方法的一個子集 -- 甚至是相當小的子集 -- 的自訂對象,就足夠“類-dictionary”而可以滿足當前的要求。不過,能顯式地整理出一個對象要用到的函數、模組、類或者架構中需要能夠做哪些事情,將是很迷人的。那就是 PyProtocols 包所做到的(一部分)。
在具有靜態型別宣告的程式設計語言中,為了在新的上下文中使用資料,您通常需要將其自一個類型 強制類型轉換(cast)或者 轉換(convert)到另一個類型。在其他語言中,轉換根據內容相關的需要隱式地進行,這些被稱為 強迫同型(coercions)。Python 中既有強制類型轉換也有強迫同型,通常使用更多的是前者(“顯式優於隱式”)。您可以將向一個浮點數加到一個整型數,結果得到一個更為通用的浮點數;但是如果您希望將字串 "3.14" 轉換為一個數字,那麼您需要使用顯式的建構函式 float("3.14") 。
PyProtocols 具有一個稱為“適配(adaptation)”的功能,類似於“部分類型(partial typing)”這一非正統電腦科學概念。適配還可能被認為是“加速的強制同型”。如果一個介面定義了所需要的一組能力 (也就是對象方法),那麼要去做“所需要的一切”的對象就要求適配 -- 通過 protocols.adapt() 函數實現 -- 以提供所需要的能力。顯然,如果您有一個顯式的轉換函式可以將類型 X 的對象轉換為類型 Y 的對象(在這裡 Y 實現了某個 IY 介面),那麼那個函數要能夠讓 X 適配協議 IY 。不過,PyProtocols 中的適配可以做比這多得多的事情。例如,甚至如果您從來沒有顯式地編寫過從類型 X 到類型 Y 的轉換程式, adapt() 通常可以推演出一條讓 X 提供 IY 所要求的能力的途徑(也就是說,找到從 X 到介面 IZ ,從 IZ 到 IW ,然後再從 IW 到 IY 的中間轉換)。
聲明介面和適配器
在 PyProtocols 中有很多不同的方法可以建立介面和適配器。PyProtocols 文檔非常詳細地介紹了這些技術 -- 很多不會在本文中涉及。接下來我們將進入一些細節,不過,我覺得,在這裡給出實際的 PyProtocols 代碼的一個最簡化執行個體是個有用的方法。
例如,我決定建立一個 Python 對象的類-Lisp 序列化。其描述並不是準確的 Lisp 文法,我也並不在意這種格式確切的優點和缺點。在這裡,我的想法只是建立一個功能,使之可以執行類似 repr() 函數或 pprint 模組的工作,不過結果是既與以前串列器(serializers)有明顯的不同,又要能更容易地擴充/定製。出於舉例說明的目的做出了一個非常不像 Lisp 的選擇:映射(mappings)是一個比列表(list)更為基礎的資料結構(Python 的元組(tuple)或列表被作為以連續整數為鍵的映射來處理)。下面是代碼:
lispy.py PyProtocol 定義
from protocols import *from cStringIO import StringIO# Like unicode, & even support objects that don't explicitly support ILispILisp = protocolForType(unicode, ['__repr__'], implicit=True)# Class for interface, but no methods specifically requiredclass ISeq(Interface): pass# Class for interface, extremely simple mapping interfaceclass 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 mappingdef 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 objdeclareAdapter(map2Lisp, provides=[ILisp], forProtocols=[IMap])# Note that a dict implements an IMap interface with no conversion neededdeclareAdapter(NO_ADAPTER_NEEDED, provides=[IMap], forTypes=[dict])# Define and use func to adapt an InstanceType obj to the ILisp interfacefrom types import InstanceTypedef 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 objclass 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 neededdeclareAdapter(NO_ADAPTER_NEEDED, provides=[ISeq], forTypes=[list, tuple])# Define a lambda func to adapt str, unicode to ILisp interfacedeclareAdapter(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 constructorclass NumberAsLisp(object): advise(instancesProvide=[ILisp], asAdapterForTypes=[long, float, complex, bool] ) def __new__(klass, val, proto): return "(%s %s)" % (val.__class__.__name__.upper(), val)
在上面的代碼中,我已經用一些不同的方法聲明了許多適配器。在一些情況中,代碼將一個介面轉換到另一個介面;在其他情況中,類型本身直接適配到另一個介面。我希望您能注意到關於代碼的一些方面:(1)沒有建立任何從 list 或 tuple 到 ILisp 介面的適配器;(2)沒有為 int 數字類型顯式聲明適配器;(3)就此而言,沒有聲明直接由 dict 到 ILisp 的適配器。下面是代碼將如何適配( adapt() )各種 Python 對象:
test_lispy.py 對象序列化
from lispy import *from sys import stdout, stderrtoLisp = 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 序列化結果
$ 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)
對我們的輸出進行一些解釋將會有所協助。第一行比較簡單,我們定義了一個直接從字串到 ILisp 的適配器,對 adapt("foo bar", ILisp) 的調用只是返回了 匿名函式的結果。下一行只是有一點複雜。沒有直接從 dict 到 ILisp 的適配器;但我們不必使用任何適配器就可以讓 dict 去適配 IMap (我們聲明了足夠多),而且我們有從 IMap 到 ILisp 的適配器。類似的,對於後面的列表和元組,我們可以使 ILisp 適配 ISeq ,使 ISeq 適配 IMap ,並使 IMap 適配 ILisp 。PyProtocols 會指出要採取的適配路徑,所有這些不可思議的過程都在幕後完成。一箇舊風格的執行個體所經曆的過程與字串或者支援 IMap 的對象相同,我們有一個直接到 ILisp 的適配。
不過,等一下。在我們的 dict 和 tuple 對象中用到的所有的整數是怎麼處理的呢? long 、 complex、float 和 bool 類型的數字有顯式的適配器,不過 int 一個都沒有。這裡的技巧在於, int 對象已經擁有一個 .__repr__() 方法;通過將隱式支援聲明為 ILisp 介面的一部分,我們可以巧妙地使用對象已有的 .__repr__() 方法作為對 ILisp 介面的支援。實際上,作為一個內建的類型,整數用不加任何修飾的阿拉伯數字表示,而不使用大寫的類型初始器(比如 LONG )。
適配協議
讓我們來更明確地看一下 protocol.adapt() 函數都做了什麼事情。在我們的例子中,我們使用“聲明 API(declaration API)”來隱式地為適配設定了一組“工廠(factories)”。這個 API 有幾個層次。聲明 API 的“基本層次(primitives)”是函數: declareAdaptorForType() 、 declareAdaptorForObject() 和 declareAdaptorForProtocol() 。前面的例子中沒有用到這些,而是用到了一些高層次的 API,如 declareImplementation() 、 declareAdaptor() 、 adviceObject() 和 protocolForType() 。在一種情況下,我們看到在一個類體中有“奇妙”的 advise() 聲明。 advise() 函數支援用於配置那些建議的類的目的和角色的大量關鍵字參數。您還可以建議 (advise()) 一個模組對象。
您不需要使用聲明 API 來建立知道如何使對象適配( adapt() )自己的可適配的對象或者介面。讓我們來看 adapt() 的調用標記,然後解釋它隨後的過程。對 adapt() 的調用類似這樣:
adapt() 的調用標記
adapt(component, protocol, [, default [, factory]])
這就表示您希望讓對象 component 去適配介面 protocol 。如果指定了 default ,它可以返回為一個封裝對象(wrapper object)或者對 component 的修改。如果 factory 被指定為一個關鍵字參數,那麼會使用一個轉換工廠來產生封裝或者修改。不過讓我們先退回一點,來看一下 adapt() 嘗試的完整的動作次序(簡化的代碼):
adapt() 的假想實現
if isinstance(component, protocol): return componentelif hasattr(component,'__conform__'): return component.__conform__(protocol)elif hasattr(protocol,'__adapt__'): return protocol.__adapt__(component)elif default is not None: return defaultelif factory is not None: return factory(component, protocol)else: NotImplementedError
對 adapt()的調用 應該保持一些特性(不過這是對程式員的建議,而不是庫的一般強制要求)。對 adapt() 的調用應該是等冪的。也就是說,對於一個對象 x 和一個協議 P ,我們希望: adapt(x,P)==adapt(adapt(x,P),P) 。進階地,這樣做的目的類似於從 .__iter__() 方法返回自身( self )的迭代器(iterator)類的目的。您基本上不會希望去重新適配到您已經適配到的相同類型以產生波動的結果。
還值得注意的是,適配可能是有損耗的。為了讓一個對象去順應一個介面,可能不方便或者不可能保持重新初始化這個對象所需要的所有資訊。也就是說,通常情況下,對對象 x 及協議 P1 和 P2 而言: adapt(x,P1)!=adapt(adapt(adapt(x,P1),P2),P1) 。
在結束之前,讓我們來看另一個利用了 adapt() 的低層次行為的測試指令碼:
test_lispy2.py 對象序列化
from lispy import *class Bar(object): passclass 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(,('__repr__',))!'(Adapt Bat to WeakSubset(,('__repr__',))!)
結果證明 lispy.py 的設計不能滿足等冪的目標。改進這一設計可能是個不錯的練習。不過,像 ILisp 這樣的描述肯定會損耗原始對象中的資訊(這是沒關係的)。
結束語
感覺上,PyProtocols 與本專欄提及的其他“外來”話題有一些共同之處。首先,聲明 API 是聲明性的(相對於解釋性)。宣告式程式設計並不給出執行一個動作所需要的步驟和開關,而是聲明處理特定的內容,由庫或編譯器來具體指出如何執行。名稱“declare*()”和“advice*()”正在來自於這一觀點。
不過,我也發現 PyProtocols 編程有些類似於使用多指派進行編程,具體說就是使用我在另一期文章提到的 gnosis.magic.multimethods 模組。與 PyProtocols 的確定適配路徑形成對照,我自己的模組執行了一個相對簡單的推演,確定要指派的相關祖先類。不過兩個庫都傾向於在編程中鼓勵使用類似的模組化思想 -- 由大量的小函數或類來執行“可插入的”任務,不需要受死板的類層級結構所困。在我看來,這種風格有其優越之處。