Python中的元類編程入門指引
回顧面向?qū)ο缶幊?/strong>
讓我們先用 30 秒鐘來回顧一下 OOP 到底是什么。在面向?qū)ο缶幊陶Z言中,可以定義 類,它們的用途是將相關(guān)的數(shù)據(jù)和行為捆綁在一起。這些類可以繼承其 父類的部分或全部性質(zhì),但也可以定義自己的屬性(數(shù)據(jù))或方法(行為)。在定義類的過程結(jié)束時,類通常充當(dāng)用來創(chuàng)建 實例(有時也簡單地稱為 對象)的模板。同一個類的不同實例通常有不同的數(shù)據(jù),但“外表”都是一樣 — 例如, Employee 對象 bob 和 jane 都有 .salary 和 .room_number ,但兩者的房間和薪水都各不相同。
一些 OOP 語言(包括 Python)允許對象是 自省的(也稱為 反射)。即,自省對象能夠描述自己:實例屬于哪個類?類有哪些祖先?對象可以用哪些方法和屬性?自省讓處理對象的函數(shù)或方法根據(jù)傳遞給函數(shù)或方法的對象類型來做決定。即使沒有自省,函數(shù)也常常根據(jù)實例數(shù)據(jù)進行劃分,例如,到 jane.room_number 的路線不同于到 bob.room_number 的路線,因為它倆在不同的房間。利用自省, 還可以在安全地計算 jane 所獲獎金的同時,跳過對 bob 的計算,例如,因為 jane 有 .profit_share 屬性,或者因為 bob 是子類 Hourly(Employee) 的實例。
元類編程(metaprogramming)的回答
以上概述的基本 OOP 系統(tǒng)功能相當(dāng)強大。但在上述描述中有一個要素沒有受到重視:在 Python(以及其它語言)中,類本身就是可以被傳遞和自省的對象。正如前面所講到的,既然可以用類作為模板來生成對象,那么用什么 作為模板來生成類呢?答案當(dāng)然是 元類(metaclass)。
Python 一直都有元類。但元類中所涉及的方法在 Python 2.2 中才得以更好地公開在人們面前。Python V2.2 明確地不再只使用一個特殊的(通常是隱藏的)元類來創(chuàng)建每個類對象?,F(xiàn)在程序員可以創(chuàng)建原始元類 type 的子類,甚至可以用各種元類動態(tài)地生成類。當(dāng)然,僅僅因為 可以在 Python 2.2 中操作元類,這并不能說明您可能想這樣做的原因。
而且,不需要使用定制元類來操作類的生成。一種不太費腦筋的概念是 類工廠:一種普通的函數(shù),它可以 返回在函數(shù)體內(nèi)動態(tài)創(chuàng)建的類。用傳統(tǒng)的 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
工廠函數(shù) class_with_method() 動態(tài)地創(chuàng)建一個類,并返回該類,這個類包含傳遞給該工廠 的方法/函數(shù)。在返回該類之前,在函數(shù)體內(nèi)操作類自身。 new 模塊提供了更簡潔的編碼方式,但其中的選項與 類工廠體內(nèi)定制代碼的選項不同,例如:
清單 2. new 模塊中的類工廠
>>> from new import classobj >>> Foo2 = classobj('Foo2',(Foo,),{'bar':lambda self:'bar'}) >>> Foo2().bar() 'bar' >>> Foo2().say_foo() foo
在所有這些情形中,沒有將類( Foo 和 Foo2 )的行為直接編寫為代碼, 而是用動態(tài)參數(shù)在運行時調(diào)用函數(shù)來創(chuàng)建類的行為。這里要強調(diào)的一點是,不僅 實例可以動態(tài)地創(chuàng)建,而且 類本身也可以動態(tài)地創(chuàng)建。
元類:尋求問題的解決方案?
元類的魔力是如此之大,以至于 99% 的用戶曾有過的顧慮都是不必要的。如果您想知道是否需要它們,則可以不用它們(那些實際需要元類的人們確實清楚自己需要它們,不需要解釋原因)。— Python 專家 Tim Peters
(類的)方法象普通函數(shù)一樣可以返回對象。所以從這個意義上講,類工廠可以是類,就象它們可以是函數(shù)一樣容易,這是顯然的。尤其 是 Python 2.2+ 提供了一個稱為 type 的特殊類,它正是這樣的類工廠。當(dāng)然,讀者會認識到 type() 不象 Python 老版本的內(nèi)置函數(shù)那樣“野心勃勃”— 幸運的是,老版本的 type() 函數(shù)的行為是由 type 類維護的(換句話說, type(obj) 返回對象 obj 的類型/類)。作為類工廠的新 type 類,其工作方式與函數(shù) new.classobj 一直所具有的方式相同:
清單 3. 作為類工廠元類的 type
>>> X = type('X',(),{'foo':lambda self:'foo'}) >>> X, X().foo() (<class '__main__.X'>, 'foo')
但是因為 type 現(xiàn)在是(元)類,所以可以自由用它來創(chuàng)建子類:
清單 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 X Init'ing (configuring) class X >>> X, X().foo() (<class '__main__.X'>, 'foo')
富有“魔力”的 .__new__() 和 .__init__() 方法很特殊,但在概念上,對于任何其它類,它們的工作方式都是一樣的。 .__init__() 方法使您能配置所創(chuàng)建的對象; .__new__() 方法使您能定制它的分配。當(dāng)然,后者沒有被廣泛地使用,但對于每個 Python 2.2 new 樣式的類(通常通過繼承而不是覆蓋),都存在該方法。
需要注意 type 后代的一個特性;它常使第一次使用元類的人們“上圈套”。按照慣例,這些方法的第一個參數(shù)名為 cls ,而不是 self ,因為這些方法是在 已生成的類上進行操作的,而不是在元類上。事實上,關(guān)于這點沒什么特別的;所有方法附加在它們的實例上,而且元類的實例是類。非特殊的 名稱使這更明顯:
清單 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_ 變量可以強制使用定制元類。但大多數(shù)時間,最安全的方法是,在希望通過定制元類來創(chuàng)建類時,設(shè)置該類的 _metaclass_ 類屬性。必須在類定義本身中設(shè)置變量,因為 如果稍后(在已經(jīng)創(chuàng)建類對象之后)設(shè)置屬性 ,則不會使用元類。例如:
清單 6. 用類屬性設(shè)置元類
>>> class Bar: ... __metaclass__ = Printable ... def foomethod(self): print 'foo' ... >>> Bar.whoami() I am a Bar >>> Bar().foomethod() foo
用這種“魔力”來解決問題
至此,我們已經(jīng)了解了一些有關(guān)元類的基本知識。但要使用元類,則比較復(fù)雜。使用元類的困難之處在于,通常在 OOP 設(shè)計中,類其實 做得不多。對于封裝和打包數(shù)據(jù)和方法,類的繼承結(jié)構(gòu)很有用,但在具體 情形中,人們通常使用實例。
我們認為元類在兩大類編程任務(wù)中確實有用。
第一類(可能是更常見的一類)是在設(shè)計時不能 確切地知道類需要做什么。顯然,您對它有所了解,但某個特殊的細節(jié) 可能取決于稍后才能得到的信息?!吧院蟆北旧碛袃深悾海╝)當(dāng)應(yīng)用程序使用庫模塊時;(b)在運行時,當(dāng)某種情形存在時。這類很接近于通常所說的“面向方面的編程(Aspect-Oriented Programming,AOP)”。我們將展示一個我們認為非常別致的示例:
清單 7. 運行時的元類配置
% cat dump.py #!/usr/bin/python import sys if 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>
正如您所期望的,該應(yīng)用程序打印出 data 對象相當(dāng)常規(guī)的描述(常規(guī)的實例對象)。但如果將 運行時參數(shù)傳遞給應(yīng)用程序,則可以得到相當(dāng)不同的結(jié)果:
清單 8. 添加外部序列化元類
% dump.py gnosis.magic MetaXMLPickler <?xml version="1.0"?> <!DOCTYPE PyObject SYSTEM "PyObjects.dtd"> <PyObject module="__main__" class="Data" id="720748"> <attr name="lst" type="list" id="980012" > <item type="string" value="a" /> <item type="string" value="b" /> <item type="string" value="c" /> </attr> <attr name="num" type="numeric" value="38" /> <attr name="str" type="string" value="spam" /> </PyObject>
這個特殊的示例使用 gnosis.xml.pickle 的序列化樣式,但最新的 gnosis.magic 包還包含元類序列化器 MetaYamlDump 、 MetaPyPickler 和 MetaPrettyPrint 。而且, dump.py “應(yīng)用程序”的用戶可以從任何定義了任何期望的 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)
這種安排的過人之處在于應(yīng)用程序程序員不需要了解要使用哪種序列化 — 甚至不需要了解是否 在命令行添加序列化或其它一些跨各部分的能力。
也許元類最常見的用法與 MetaPickler 類似:添加、刪除、重命名或替換所產(chǎn)生類中定義的方法。在我們的示例中,在創(chuàng)建類 Data (以及由此再創(chuàng)建隨后的每個實例)時,“本機” Data.dump() 方法被應(yīng)用程序之外的某個方法所替代。
使用這種“魔力”來解決問題的其它方法
存在著這樣的編程環(huán)境:類往往比實例更重要。例如, 說明性迷你語言(declarative mini-languages)是 Python 庫,在類聲明中直接表示了它的程序邏輯。David 在其文章“ Create declarative mini-languages”中研究了此問題。在這種情形下,使用元類來影響類創(chuàng)建過程是相當(dāng)有用的。
一種基于類的聲明性框架是 gnosis.xml.validity 。 在此框架下,可以聲明許多“有效性類”,這些類表示了一組有關(guān)有效 XML 文檔的約束。這些聲明非常接近于 DTD 中所包含的那些聲明。例如,可以用以下代碼來配置一篇“dissertation”文檔:
清單 10. simple_diss.py gnosis.xml.validity 規(guī)則
from gnosis.xml.validity import * class figure(EMPTY): pass class _mixedpara(Or): _disjoins = (PCDATA, figure) class paragraph(Some): _type = _mixedpara class title(PCDATA): pass class _paras(Some): _type = paragraph class chapter(Seq): _order = (title, _paras) class dissertation(Some): _type = chapter
如果在沒有正確組件子元素的情形下嘗試實例化 dissertation 類,則會產(chǎn)生一個描述性異常;對于每個 子元素,亦是如此。當(dāng)只有一種明確的方式可以將參數(shù)“提升”為正確的類型 時,會從較簡單的參數(shù)來生成正確的子元素。
即使有效性類常常(非正式)基于預(yù)先存在的 DTD,這些類的實例也還是將自己打印成簡單的 XML 文檔片段,例如:
清單 11. 基本的有效性類文檔的創(chuàng)建
>>> from simple_diss import * >>> ch = LiftSeq(chapter, ('It Starts','When it began')) >>> print ch <chapter><title>It Starts</title> <paragraph>When it began</paragraph> </chapter>
通過使用元類來創(chuàng)建有效性類,我們可以從類聲明中生成 DTD(我們在這樣做的同時,可以向這些有效性類額外添加一個方法):
清單 12. 在模塊導(dǎo)入期間利用元類
>>> 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'?> <!DOCTYPE chapter [ <!ELEMENT figure EMPTY> <!ELEMENT dissertation (chapter)+> <!ELEMENT chapter (title,paragraph+)> <!ELEMENT title (#PCDATA)> <!ELEMENT paragraph ((#PCDATA|figure))+> ]> <chapter><title>It Starts</title> <paragraph>When it began</paragraph> </chapter>
包 gnosis.xml.validity 不知道 DTD 和內(nèi)部子集。那些概念和能力完全由元類 DTDGenerator 引入進來,對 gnosis.xml.validity 或 simple_diss.py 不做 任何更改。 DTDGenerator 不將自身的 .__str__() 方法替換進它產(chǎn)生的類 — 您仍然可以打印簡單的 XML 片段 — 但元類可以方便地修改這種富有“魔力”的方法。
元帶來的便利
為了使用元類以及一些可以在面向方面的編程中所使用的樣本元類,包 gnosis.magic 包含幾個實用程序。其中最 重要的實用程序是 import_with_metaclass() 。 在上例中所用到的這個函數(shù)使您能導(dǎo)入第三方的模塊,但您要用定制元類而不是用 type 來創(chuàng)建所有模塊類。無論您想對第三方模塊賦予什么樣的新能力,您都可以在創(chuàng)建的元類內(nèi)定義該能力(或者從其它地方一起獲得)。 gnosis.magic 包含一些可插入的序列化元類;其它一些包可能包含跟蹤能力、對象持久性、異常日志記錄或其它能力。
import_with_metclass() 函數(shù)展示了元類編程的幾個性質(zhì):
清單 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
在這個函數(shù)中值得注意的樣式是,用指定的元類生成普通的類 Meta 。但是,一旦將 Meta 作為祖先添加之后,也用定制元類來生成它的后代。原則上,象 Meta 這樣的類 既可以帶有元類生成器(metaclass producer) 也可以帶有一組可繼承的方法 — Meta 類的這兩個方面是無關(guān)的。
相關(guān)文章
pytorch 如何把圖像數(shù)據(jù)集進行劃分成train,test和val
這篇文章主要介紹了pytorch 把圖像數(shù)據(jù)集進行劃分成train,test和val的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-05-05keras Lambda自定義層實現(xiàn)數(shù)據(jù)的切片方式,Lambda傳參數(shù)
這篇文章主要介紹了keras Lambda自定義層實現(xiàn)數(shù)據(jù)的切片方式,Lambda傳參數(shù),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-06-06Python抓新型冠狀病毒肺炎疫情數(shù)據(jù)并繪制全國疫情分布的代碼實例
在本篇文章里小編給大家整理了一篇關(guān)于Python抓新型冠狀病毒肺炎疫情數(shù)據(jù)并繪制全國疫情分布的代碼實例,有興趣的朋友們可以學(xué)習(xí)下。2020-02-02Python游戲開發(fā)實例之graphics實現(xiàn)AI五子棋
五子棋是經(jīng)典的棋牌類游戲,很多人都玩過,那么如何用Python實現(xiàn)五子棋呢,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-113行Python代碼實現(xiàn)圖像照片摳圖和換底色的方法
這篇文章主要介紹了3行Python代碼實現(xiàn)圖像照片摳圖和換底色的方法,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10Python Web框架Flask下網(wǎng)站開發(fā)入門實例
這篇文章主要介紹了Python Web框架Flask下網(wǎng)站開發(fā)入門實例,本文實現(xiàn)了一個注冊頁面、登錄頁面和上傳頁面,需要的朋友可以參考下2015-02-02Python基礎(chǔ)教程之tcp socket編程詳解及簡單實例
這篇文章主要介紹了Python基礎(chǔ)教程之tcp socket編程詳解及簡單實例的相關(guān)資料,需要的朋友可以參考下2017-02-02Python xlwt設(shè)置excel單元格字體及格式
這篇文章主要為大家詳細介紹了Python xlwt設(shè)置excel單元格字體及格式的方法,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-12-12