回顧物件導向編程
讓我們先用 30 秒鐘來回顧一下 OOP 到底是什麼。在物件導向程式設計語言中,可以定義 類,它們的用途是將相關的資料和行為捆綁在一起。這些類可以繼承其 父類的部分或全部性質,但也可以定義自己的屬性(資料)或方法(行為)。在定義類的過程結束時,類通常充當用來建立 執行個體(有時也簡單地稱為 對象)的模板。同一個類的不同執行個體通常有不同的資料,但“外表”都是一樣 — 例如, Employee 對象 bob 和 jane 都有 .salary 和 .room_number ,但兩者的房間和薪水都各不相同。
一些 OOP 語言(包括 Python)允許對象是 自省的(也稱為 反射)。即,自省對象能夠描述自己:執行個體屬於哪個類?類有哪些祖先?對象可以用哪些方法和屬性?自省讓處理對象的函數或方法根據傳遞給函數或方法的物件類型來做決定。即使沒有自省,函數也常常根據執行個體資料進行劃分,例如,到 jane.room_number 的路線不同於到 bob.room_number 的路線,因為它倆在不同的房間。利用自省, 還可以在安全地計算 jane 所獲獎金的同時,跳過對 bob 的計算,例如,因為 jane 有 .profit_share 屬性,或者因為 bob 是子類 Hourly(Employee) 的執行個體。
元類編程(metaprogramming)的回答
以上概述的基本 OOP 系統功能相當強大。但在上述描述中有一個要素沒有受到重視:在 Python(以及其它語言)中,類本身就是可以被傳遞和自省的對象。正如前面所講到的,既然可以用類作為模板來產生對象,那麼用什麼 作為模板來產生類呢?答案當然是 元類(metaclass)。
Python 一直都有元類。但元類中所涉及的方法在 Python 2.2 中才得以更好地公開在人們面前。Python V2.2 明確地不再只使用一個特殊的(通常是隱藏的)元類來建立每個類對象。現在程式員可以建立原始元類 type 的子類,甚至可以用各種元類動態地產生類。當然,僅僅因為 可以在 Python 2.2 中操作元類,這並不能說明您可能想這樣做的原因。
而且,不需要使用定製元類來操作類的產生。一種不太費腦筋的概念是 類工廠:一種普通的函數,它可以 返回在函數體內動態建立的類。用傳統的 Python 文法,您可以編寫:
清單 1. 老式的 Python 1.5.2 類工廠
Python 1.5.2 (#0, Jun 27 1999, 11:23:01) [...]Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam>>> def class_with_method(func):... class klass: pass... setattr(klass, func.__name__, func)... return klass...>>> def say_foo(self): print 'foo'...>>> Foo = class_with_method(say_foo)>>> foo = Foo()>>> foo.say_foo()foo
工廠函數 class_with_method() 動態地建立一個類,並返回該類,這個類包含傳遞給該工廠 的方法/函數。在返回該類之前,在函數體內操作類自身。 new 模組提供了更簡潔的編碼方式,但其中的選項與 類工廠體內定製代碼的選項不同,例如:
清單 2. new 模組中的類工廠
>>> from new import classobj>>> Foo2 = classobj('Foo2',(Foo,),{'bar':lambda self:'bar'})>>> Foo2().bar()'bar'>>> Foo2().say_foo()foo
在所有這些情形中,沒有將類( Foo 和 Foo2 )的行為直接編寫為代碼, 而是用動態參數在運行時調用函數來建立類的行為。這裡要強調的一點是,不僅 執行個體可以動態地建立,而且 類本身也可以動態地建立。
元類:尋求問題的解決方案?
元類的魔力是如此之大,以至於 99% 的使用者曾有過的顧慮都是不必要的。如果您想知道是否需要它們,則可以不用它們(那些實際需要元類的人們確實清楚自己需要它們,不需要解釋原因)。— Python 專家 Tim Peters
(類的)方法象普通函數一樣可以返回對象。所以從這個意義上講,類工廠可以是類,就象它們可以是函數一樣容易,這是顯然的。尤其 是 Python 2.2+ 提供了一個稱為 type 的特殊類,它正是這樣的類工廠。當然,讀者會認識到 type() 不象 Python 老版本的內建函數那樣“野心勃勃”— 幸運的是,老版本的 type() 函數的行為是由 type 類維護的(換句話說, type(obj) 返回對象 obj 的類型/類)。作為類工廠的新 type 類,其工作方式與函數 new.classobj 一直所具有的方式相同:
清單 3. 作為類工廠元類的 type
>>> X = type('X',(),{'foo':lambda self:'foo'})>>> X, X().foo()(, 'foo')
但是因為 type 現在是(元)類,所以可以自由用它來建立子類:
清單 4. 作為類工廠的 type 後代
>>> class ChattyType(type):... def __new__(cls, name, bases, dct):... print "Allocating memory for class", name... return type.__new__(cls, name, bases, dct)... def __init__(cls, name, bases, dct):... print "Init'ing (configuring) class", name... super(ChattyType, cls).__init__(name, bases, dct)...>>> X = ChattyType('X',(),{'foo':lambda self:'foo'})Allocating memory for class XInit'ing (configuring) class X>>> X, X().foo()(, 'foo')
富有“魔力”的 .__new__() 和 .__init__() 方法很特殊,但在概念上,對於任何其它類,它們的工作方式都是一樣的。 .__init__() 方法使您能配置所建立的對象; .__new__() 方法使您能定製它的分配。當然,後者沒有被廣泛地使用,但對於每個 Python 2.2 new 樣式的類(通常通過繼承而不是覆蓋),都存在該方法。
需要注意 type 後代的一個特性;它常使第一次使用元類的人們“上圈套”。按照慣例,這些方法的第一個參數名為 cls ,而不是 self ,因為這些方法是在 已產生的類上進行操作的,而不是在元類上。事實上,關於這點沒什麼特別的;所有方法附加在它們的執行個體上,而且元類的執行個體是類。非特殊的 名稱使這更明顯:
清單 5. 將類方法附加在所產生的類上
>>> class Printable(type):... def whoami(cls): print "I am a", cls.__name__...>>> Foo = Printable('Foo',(),{})>>> Foo.whoami()I am a Foo>>> Printable.whoami()Traceback (most recent call last):TypeError: unbound method whoami() [...]
所有這些令人驚訝但又常見的做法以及便於掌握的文法使得元類的使用更容易,但也讓新使用者感到迷惑。對於其它文法有幾個元素。但這些新變體的解析順序需要點技巧。類可以從其祖先那繼承元類 — 請注意,這與將元類 作為祖先 不一樣(這是另一處常讓人迷惑的地方)。對於老式類,定義一個全域 _metaclass_ 變數可以強制使用定製元類。但大多數時間,最安全的方法是,在希望通過定製元類來建立類時,設定該類的 _metaclass_ 類屬性。必須在類定義本身中設定變數,因為 如果稍後(在已經建立類對象之後)設定屬性 ,則不會使用元類。例如:
清單 6. 用類屬性設定元類
>>> class Bar:... __metaclass__ = Printable... def foomethod(self): print 'foo'...>>> Bar.whoami()I am a Bar>>> Bar().foomethod()foo
用這種“魔力”來解決問題
至此,我們已經瞭解了一些有關元類的基本知識。但要使用元類,則比較複雜。使用元類的困難之處在於,通常在 OOP 設計中,類其實 做得不多。對於封裝和打包資料和方法,類的繼承結構很有用,但在具體 情形中,人們通常使用執行個體。
我們認為元類在兩大類編程任務中確實有用。
第一類(可能是更常見的一類)是在設計時不能 確切地知道類需要做什麼。顯然,您對它有所瞭解,但某個特殊的細節 可能取決於稍後才能得到的資訊。“稍後”本身有兩類:(a)當應用程式使用庫模組時;(b)在運行時,當某種情形存在時。這類很接近於通常所說的“面向方面的編程(Aspect-Oriented Programming,AOP)”。我們將展示一個我們認為非常別緻的樣本:
清單 7. 運行時的元類配置
% cat dump.py#!/usr/bin/pythonimport sysif len(sys.argv) > 2: module, metaklass = sys.argv[1:3] m = __import__(module, globals(), locals(), [metaklass]) __metaclass__ = getattr(m, metaklass)class Data: def __init__(self): self.num = 38 self.lst = ['a','b','c'] self.str = 'spam' dumps = lambda self: `self` __str__ = lambda self: self.dumps()data = Data()print data% dump.py<__main__.Data instance at 1686a0>
正如您所期望的,該應用程式列印出 data 對象相當常規的描述(常規的執行個體對象)。但如果將 運行時參數傳遞給應用程式,則可以得到相當不同的結果:
清單 8. 添加外部序列化元類
% dump.py gnosis.magic MetaXMLPickler<?xml version="1.0"?>
這個特殊的樣本使用 gnosis.xml.pickle 的序列化樣式,但最新的 gnosis.magic 包還包含元類序列化器 MetaYamlDump 、 MetaPyPickler 和 MetaPrettyPrint 。而且, dump.py “應用程式”的使用者可以從任何定義了任何期望的 MetaPickler 的 Python 包中利用該“MetaPickler”。出於此目的而 編寫合適的元類如下所示:
清單 9. 用元類添加屬性
class MetaPickler(type): "Metaclass for gnosis.xml.pickle serialization" def __init__(cls, name, bases, dict): from gnosis.xml.pickle import dumps super(MetaPickler, cls).__init__(name, bases, dict) setattr(cls, 'dumps', dumps)
這種安排的過人之處在於應用程式程式員不需要瞭解要使用哪種序列化 — 甚至不需要瞭解是否 在命令列添加序列化或其它一些跨各部分的能力。
也許元類最常見的用法與 MetaPickler 類似:添加、刪除、重新命名或替換所產生類中定義的方法。在我們的樣本中,在建立類 Data (以及由此再建立隨後的每個執行個體)時,“本機” Data.dump() 方法被應用程式之外的某個方法所替代。
使用這種“魔力”來解決問題的其它方法
存在著這樣的編程環境:類往往比執行個體更重要。例如, 說明性迷你語言(declarative mini-languages)是 Python 庫,在類聲明中直接表示了它的程式邏輯。David 在其文章“ Create declarative mini-languages”中研究了此問題。在這種情形下,使用元類來影響類建立過程是相當有用的。
一種基於類的聲明性架構是 gnosis.xml.validity 。 在此架構下,可以聲明許多“有效性類”,這些類表示了一組有關有效 XML 文檔的約束。這些聲明非常接近於 DTD 中所包含的那些聲明。例如,可以用以下代碼來配置一篇“dissertation”文檔:
清單 10. simple_diss.py gnosis.xml.validity 規則
from gnosis.xml.validity import *class figure(EMPTY): passclass _mixedpara(Or): _disjoins = (PCDATA, figure)class paragraph(Some): _type = _mixedparaclass title(PCDATA): passclass _paras(Some): _type = paragraphclass chapter(Seq): _order = (title, _paras)class dissertation(Some): _type = chapter
如果在沒有正確組件子項目的情形下嘗試執行個體化 dissertation 類,則會產生一個描述性異常;對於每個 子項目,亦是如此。當只有一種明確的方式可以將參數“提升”為正確的類型 時,會從較簡單的參數來產生正確的子項目。
即使有效性類常常(非正式)基於預先存在的 DTD,這些類的執行個體也還是將自己列印成簡單的 XML 文檔片段,例如:
清單 11. 基本的有效性類文檔的建立
>>> from simple_diss import *>>> ch = LiftSeq(chapter, ('It Starts','When it began'))>>> print chIt StartsWhen it began
通過使用元類來建立有效性類,我們可以從類聲明中產生 DTD(我們在這樣做的同時,可以向這些有效性類額外添加一個方法):
清單 12. 在模組匯入期間利用元類
>>> from gnosis.magic import DTDGenerator, \... import_with_metaclass, \... from_import>>> d = import_with_metaclass('simple_diss',DTDGenerator)>>> from_import(d,'**')>>> ch = LiftSeq(chapter, ('It Starts','When it began'))>>> print ch.with_internal_subset()<?xml version='1.0'?>]>It StartsWhen it began
包 gnosis.xml.validity 不知道 DTD 和內部子集。那些概念和能力完全由元類 DTDGenerator 引入進來,對 gnosis.xml.validity 或 simple_diss.py 不做 任何更改。 DTDGenerator 不將自身的 .__str__() 方法替換進它產生的類 — 您仍然可以列印簡單的 XML 片段 — 但元類可以方便地修改這種富有“魔力”的方法。
元帶來的便利
為了使用元類以及一些可以在面向方面的編程中所使用的樣本元類,包 gnosis.magic 包含幾個公用程式。其中最 重要的公用程式是 import_with_metaclass() 。 在上例中所用到的這個函數使您能匯入第三方的模組,但您要用定製元類而不是用 type 來建立所有模組類。無論您想對第三方模組賦予什麼樣的新能力,您都可以在建立的元類內定義該能力(或者從其它地方一起獲得)。 gnosis.magic 包含一些可插入的序列化元類;其它一些包可能包含跟蹤能力、對象持久性、異常日誌記錄或其它能力。
import_with_metclass() 函數展示了元類編程的幾個性質:
清單 13. [gnosis.magic] 的 import_with_metaclass()
def import_with_metaclass(modname, metaklass): "Module importer substituting custom metaclass" class Meta(object): __metaclass__ = metaklass dct = {'__module__':modname} mod = __import__(modname) for key, val in mod.__dict__.items(): if inspect.isclass(val): setattr(mod, key, type(key,(val,Meta),dct)) return mod
在這個函數中值得注意的樣式是,用指定的元類產生普通的類 Meta 。但是,一旦將 Meta 作為祖先添加之後,也用定製元類來產生它的後代。原則上,象 Meta 這樣的類 既可以帶有元類產生器(metaclass producer) 也可以帶有一組可繼承的方法 — Meta 類的這兩個方面是無關的。