1. 程式人生 > >深刻理解Python中的元類(metaclass)

深刻理解Python中的元類(metaclass)

譯註:這是一篇在Stack overflow上很熱的帖子。提問者自稱已經掌握了有關Python OOP程式設計中的各種概念,但始終覺得元類(metaclass)難以理解。他知道這肯定和自省有關,但仍然覺得不太明白,希望大家可以給出一些實際的例子和程式碼片段以幫助理解,以及在什麼情況下需要進行超程式設計。於是e-satis同學給出了神一般的回覆,該回復獲得了985點的贊同點數,更有人評論說這段回覆應該加入到Python的官方文件中去。而e-satis同學本人在Stack Overflow中的聲望積分也高達64271分。以下就是這篇精彩的回覆(提示:非常長)

類也是物件

在理解元類之前,你需要先掌握Python中的類。Python中類的概念借鑑於Smalltalk,這顯得有些奇特。在大多數程式語言中,類就是一組用來描述如何生成一個物件的程式碼段。在Python中這一點仍然成立:

Python
123456 >>>classObjectCreator(
object):pass>>>my_object=ObjectCreator()>>>printmy_object<__main__.ObjectCreator objectat0x8974f2c>

但是,Python中的類還遠不止如此。類同樣也是一種物件。是的,沒錯,就是物件。只要你使用關鍵字class,Python直譯器在執行的時候就會建立一個物件。下面的程式碼段:

Python
123 >>>classObjectCreator(object):pass

將在記憶體中建立一個物件,名字就是ObjectCreator。這個物件(類)自身擁有建立物件(類例項)的能力,而這就是為什麼它是一個類的原因。但是,它的本質仍然是一個物件,於是乎你可以對它做如下的操作:

1)   你可以將它賦值給一個變數

2)   你可以拷貝它

3)   你可以為它增加屬性

4)   你可以將它作為函式引數進行傳遞

下面是示例:

Python
1234567891011121314151617 >>>printObjectCreator# 你可以列印一個類,因為它其實也是一個物件<class'__main__.ObjectCreator'>>>>defecho(o):printo>>>echo(ObjectCreator)# 你可以將類做為引數傳給函式<class'__main__.ObjectCreator'>>>>printhasattr(ObjectCreator,'new_attribute')Fasle>>>ObjectCreator.new_attribute='foo'#  你可以為類增加屬性>>>printhasattr(ObjectCreator,'new_attribute')True>>>printObjectCreator.new_attributefoo>>>ObjectCreatorMirror=ObjectCreator# 你可以將類賦值給一個變數>>>printObjectCreatorMirror()<__main__.ObjectCreator objectat0x8997b4c>

動態地建立類

因為類也是物件,你可以在執行時動態的建立它們,就像其他任何物件一樣。首先,你可以在函式中建立類,使用class關鍵字即可。

Python
123456789101112131415 >>>defchoose_class(name):ifname=='foo':classFoo(object):passreturnFoo# 返回的是類,不是類的例項else:classBar(object):passreturnBar>>>MyClass=choose_class('foo')>>>printMyClass# 函式返回的是類,不是類的例項<class'__main__'.Foo>>>>printMyClass()# 你可以通過這個類建立類例項,也就是物件<__main__.Foo objectat0x89c6d4c>

但這還不夠動態,因為你仍然需要自己編寫整個類的程式碼。由於類也是物件,所以它們必須是通過什麼東西來生成的才對。當你使用class關鍵字時,Python直譯器自動建立這個物件。但就和Python中的大多數事情一樣,Python仍然提供給你手動處理的方法。還記得內建函式type嗎?這個古老但強大的函式能夠讓你知道一個物件的型別是什麼,就像這樣:

Python
12345678 >>>printtype(1)<type'int'>>>>printtype("1")<type'str'>>>>printtype(ObjectCreator)<type'type'>>>>printtype(ObjectCreator())<class'__main__.ObjectCreator'>

這裡,type有一種完全不同的能力,它也能動態的建立類。type可以接受一個類的描述作為引數,然後返回一個類。(我知道,根據傳入引數的不同,同一個函式擁有兩種完全不同的用法是一件很傻的事情,但這在Python中是為了保持向後相容性)

type可以像這樣工作:

Python
1 type(類名,父類的元組(針對繼承的情況,可以為空),包含屬性的字典(名稱和值))

比如下面的程式碼:

Python
12 >>>classMyShinyClass(object):pass

可以手動像這樣建立:

Python
12345 >>>MyShinyClass=type('MyShinyClass',(),{})# 返回一個類物件>>>printMyShinyClass<class'__main__.MyShinyClass'>>>>printMyShinyClass()#  建立一個該類的例項<__main__.MyShinyClass objectat0x8997cec>

你會發現我們使用“MyShinyClass”作為類名,並且也可以把它當做一個變數來作為類的引用。類和變數是不同的,這裡沒有任何理由把事情弄的複雜。

type 接受一個字典來為類定義屬性,因此

Python
12 >>>classFoo(object):bar=True

可以翻譯為:

Python
1 >>>Foo=type('Foo',(),{'bar':True})

並且可以將Foo當成一個普通的類一樣使用:

Python
123456789 >>>printFoo<class'__main__.Foo'>>>>printFoo.barTrue>>>f=Foo()>>>printf<__main__.Foo objectat0x8a9b84c>>>>printf.barTrue

當然,你可以向這個類繼承,所以,如下的程式碼:

Python
12 >>>classFooChild(Foo):pass

就可以寫成:

Python
12345 >>>FooChild=type('FooChild',(Foo,),{})>>>printFooChild<class'__main__.FooChild'>>>>printFooChild.bar# bar屬性是由Foo繼承而來True

最終你會希望為你的類增加方法。只需要定義一個有著恰當簽名的函式並將其作為屬性賦值就可以了。

Python
1234567891011 >>>defecho_bar(self):printself.bar>>>FooChild=type('FooChild',(Foo,),{'echo_bar':echo_bar})>>>hasattr(Foo,'echo_bar')False>>>hasattr(FooChild,'echo_bar')True>>>my_foo=FooChild()>>>my_foo.echo_bar()True

你可以看到,在Python中,類也是物件,你可以動態的建立類。這就是當你使用關鍵字class時Python在幕後做的事情,而這就是通過元類來實現的。

到底什麼是元類(終於到主題了)

元類就是用來建立類的“東西”。你建立類就是為了建立類的例項物件,不是嗎?但是我們已經學習到了Python中的類也是物件。好吧,元類就是用來建立這些類(物件)的,元類就是類的類,你可以這樣理解 為:

Python
12 MyClass=MetaClass()MyObject=MyClass()

你已經看到了type可以讓你像這樣做:

Python
1 MyClass=type('MyClass',(),{})

這是因為函式type實際上是一個元類。type就是Python在背後用來建立所有類的元類。現在你想知道那為什麼type會全部採用小寫形式而不是Type呢?好吧,我猜這是為了和str保持一致性,str是用來建立字串物件的類,而int是用來建立整數物件的類。type就是建立類物件的類。你可以通過檢查__class__屬性來看到這一點。Python中所有的東西,注意,我是指所有的東西——都是物件。這包括整數、字串、函式以及類。它們全部都是物件,而且它們都是從一個類建立而來。

Python
12345678910111213 >>>age=35>>>age.__class__<type'int'>>>>name='bob