本節書摘來自華章出版社《python程式設計實戰:運用設計模式、并發和程式庫建立高品質程式》一 書中的第2章,第2.1節,作者:(美) mark summerfield,更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視。
“擴充卡模式”(adapter pattern)是一種接口适配技術,可通過某個類來使用另一個接口與之不相容的類,運用此模式時,兩個類的接口都無須改動。這項技術非常有用,比方說,我們想把某個類從其原先的應用場景中拿出來放在另一個環境下運作,而這個類又不能修改,那就可以考慮擴充卡模式。
假設有個簡單的page類用于渲染頁面,它需要知道标題、正文段落以及“渲染器類”(renderer class)的執行個體。(本節代碼均選自render1.py範例程式。)

page類并不知道也無須關心傳進來的渲染器類執行個體具體是什麼,它隻要知道渲染器提供了渲染頁面所需的接口就好,也就是說,渲染器類應該有三個方法:header(str)、paragraph(str)、footer()。
在本例中,我們需要保證__init__()接收到的renderer參數确實是個renderer執行個體。有一種簡單但是很糟糕的辦法,就是用assert isinstance(renderer, renderer)語句來判斷。這麼做有兩個缺陷。首先,它抛出的是assertionerror,而不是我們所期望的typeerror,後者更為具體。其次,假如運作程式時指定了-o選項(“optimize”,優化),那麼assert語句就不會執行,而稍後執行render()方法時,将會導緻attributeerror異常。範例代碼中的if not isinstance(...)語句則沒有這兩個問題,它可以抛出typeerror異常,而且在加了-o選項後依然能正确運作。
但這種寫法也有個明顯的問題,那就是所有渲染器子類似乎都必須繼承自renderer基類。假如用c++語言來程式設計,那确實如此,而在python語言裡也是可以建立這種基類的。不過,python的abc(abstract base class,抽象基類)子產品提供了另一種做法,既能像抽象基類那樣檢查接口是否比對,又能像“動态類型”(duck typing)那樣非常靈活。這就是說,我們可以在無須繼承特定基類的前提下,建立出符合某套接口(也就是具備特定api)的對象來。
renderer類重新實作了__subclasshook__()這個“特殊方法”(special method)。python語言内置的isinstance()函數要通過此方法來決定函數的首個參數是不是第二個參數的子類(如果第二個參數是由類所構成的元組,那就判斷首個參數是不是元組中某個類的子類)。
上面這段代碼有些棘手,它必須在python 3.3及之後的版本上才能運作,因為其中用到了collections.chainmap類。這段代碼的原理稍後解釋,但就算不明白也無關緊要,因為這些複雜的操作都可以通過範例代碼中的@qtrac.has_methods“類裝飾器”(class decorator)來完成,2.2節将會示範其用法。
__subclasshook__()特殊方法首先通過class參數判斷該自己是不是在renderer類上面調用的,如果不是,就傳回notimplemented。這麼做意味着子類無法繼承__subclasshook__()的行為。由于我們假定子類要在抽象基類的基礎上添加新的标準而不是新的行為,是以才設計成這樣。若想繼承__subclasshook__()的行為也可以,隻要在重新實作__subclasshook__()的時候調用renderer.__subclasshook__()就可以了。
此方法如果傳回true或false,那麼isinstance()的判定流程就會在這個抽象基類處終止,并傳回bool值。若傳回notimplemented,則會沿着繼承體系按照通常的規則繼續判定下去(判斷subclass是不是本類的子類、是不是“顯式注冊類”(explicitly registered class)的子類、是不是子類的子類)。
如果滿足了if語句的判斷條件,那就調用subclass的__mro__()特殊方法,并周遊subclass及所有超類(包括subclass本身)的私有字典(也就是__dict__)。周遊好的字典會放在元組中,我們通過序列解包操作(*)将其傳給collections.chainmap()函數。此函數會建立一份map視圖,把它從參數中收到的所有映射表(比如字典就可以當作映射表傳進去)都當成一張映射表看待。接下來,将待檢測的方法放在另一個元組中。最後,周遊元組中的方法,判斷它們是不是都在attributes映射表中,這張映射表的鍵是subclass及其全部超類的所有方法名與屬性名。如果methods中的每個方法都在attributes映射表裡,那就傳回true。
請注意,上面這段代碼隻檢測了subclass及其全部基類的所有attribute名稱是不是涵蓋了我們所需的那些方法,并沒有詳細判斷attribute到底是屬性還是方法。如果某屬性恰好與所需方法同名,那它也能通過檢測。假如檢測時想排除屬性名而隻考慮方法名,那麼可在method in attributes這行判斷語句中加上and callable(method)。由于此問題在實際程式設計中很少遇到,是以沒必要專門改寫。
用__subclasshook__()來建立帶有接口檢查功能的類是一項非常有用的技術,但如果每個類都要寫這十幾行代碼的話,那就顯得重複了,因為這些類之間的差别可能不大,隻是基類與所支援的方法不同而已。在下一節中,我們将通過類裝飾器來避免重複代碼,也就是說,有了類裝飾器之後,每次隻需編寫一兩行特殊代碼,就能建立出具備接口檢查功能的類。(下一節的render2.py範例程式示範了這種裝飾器的用法。)
上面這個簡單的類可以當成頁面渲染器來用,因為它具備相關接口。
header()方法會根據指定的寬度把标題輸出到正中位置,然後換一行,在标題的每個字母下面輸出“=”字元。
paragraph()方法使用python标準庫的textwrap子產品把文本段按照指定的寬度換行,并列印出來。使用self.previous這個boolean變量是為了保證除第一段以外,後面每兩段之間都有空行隔開。由于頁面渲染器的接口定義了footer(),是以即便不列印頁腳,也得寫個什麼事都不做的方法放在這裡。
htmlwriter類可用來寫出簡單的html頁面,它用html.escape()函數處理轉義字元(在python 3.2及早前版本中,使用xml.sax.saxutil.escape()函數)。
盡管這個類也有header()及footer()方法,但其行為卻和頁面渲染器接口所定義的不同。是以,在建構page執行個體時,我們可以傳入textrenderer對象,但卻不能直接把htmlwriter對象當成頁面渲染器傳進去。
一種解決辦法是編寫htmlwriter的子類,并在子類中提供頁面渲染器所需的接口方法。但這種方案很容易出錯,因為子類會把htmlwriter的方法同頁面渲染器的接口方法混在一起。還有個更好的辦法就是建立擴充卡,令其把我們要使用的htmlwriter類“聚合”(aggregate)進來,并提供renderer所定義的接口,然後将聚合進來的類與接口适配好。圖2.1示範了如何引入擴充卡類。
上面列出的就是擴充卡類。在構造時,可把htmlwriter對象當成htmlwriter參數傳入,該類負責提供頁面渲染器接口所需的方法。由于實際渲染任務都會委托給聚合進來的htmlwriter對象,是以htmlrenderer類隻相當于在現有的htmlwriter類的外圍包了一層新的接口而已。
上面幾行代碼示範了如何用兩種渲染器來建立page類的執行個體。在建構textrenderer時,我們将預設寬度設為22個字元。而在用htmlrenderer來建立htmlwriter擴充卡時,我們則把一份打開的檔案傳了進去(建立此檔案所用的語句沒有列出來),這樣的話,html就不會渲染到預設的sys.stdout上面了。