寫在前面
這兩天仔細研究了python中元類的概念,從最開始的一頭霧水,到現(xiàn)在的漸漸有一點明白。想借這篇文章來闡述一下我對于python中元類的一些粗淺見解,同時也希望能給其他人一些啟發(fā),共同進步。首先,我想說在理解元類時一定要在大腦中時刻按照OOP的編程理念對程序進行分析才能夠理解元類中一些比較難以理解的地方,這也是筆者學(xué)習(xí)元類的一個小經(jīng)驗。
什么是元類
元類在python中最簡單的定義就是——type的子類。為了弄清楚這個模糊的定義,首先要弄清楚的是什么是type類,這對于后面的理解是非常重要的。type顧名思義就是“類型”的意思,它和python標(biāo)準(zhǔn)庫中的其他類一樣也是一個類,但是它也有非常特殊的地方。其他的類的實例就是一個普通的實例,而類型的實例是一個類。這是python3.0中一個更新的點,在2.X版本的python中類型的實例還不完全是一個類,在某些場景下它的實例也是一個普通的實例,不過今天我們要談的不是2.X版本中的type。讓我們用幾句簡單的代碼來驗證一下:

在代碼中首先構(gòu)造了一個list實例,然后用type的構(gòu)造函數(shù)以list的實例為參數(shù)構(gòu)造了一個type的實例,打印后我們發(fā)現(xiàn)type的實例是一個類,這和list實例的類(l.__class__)是相同的。這就印證了我們的一個論斷——類型的實例就是類。
熟悉OOP的人特別是C++的人都應(yīng)該了解,類只是聲明而不是實例,只有在程序中實例化一個類才會分配內(nèi)存。但是python中類也是一個叫做類對象的對象,它不是憑空出現(xiàn)在程序中的,它和實例一樣也需要有代碼對類對象進行實例化,說穿了類對象應(yīng)該也是一種特殊的實例,為什么這么說呢?圖一中的代碼應(yīng)該能給我們些許暗示:通過type的構(gòu)造函數(shù)構(gòu)造出類型的實例是一個類,或者說類對象(不能再細說了,再說就繞進去了,讀者自己意會吧)
那么,寫到這里我們就可以說type類負(fù)責(zé)類對象的創(chuàng)建,一般情況下這種創(chuàng)建是隱式的不被我們發(fā)覺的。而如果我們要顯式地觀察這個過程就可以通過創(chuàng)建元類來實現(xiàn)。在OOP中攔截一個類方法的方式之一就是繼承這個類并且重載需要攔截的方法,那么這里就可以引出最開始給出的元類定義,元類就是type的子類。元類通過繼承type類,進而重載type類中的一些方法來達到控制類對象生成的目的,這是元類編程中一個大體的思想。
類對象的創(chuàng)建過程
類對象的創(chuàng)建過程和實例的創(chuàng)建過程相似或者說大體上是一致的,我們通過一個例子來了解吧。

打印效果如下:

可見在類對象創(chuàng)建時首先調(diào)用元類中的__new__方法,然后調(diào)用__init__方法。其中__new__方法返回類對象的實例,__init__方法對類對象進行一些初始化。這兩個方法都是type類中的方法,在這里我們繼承type類后重載這兩個方法等于覆蓋了type類中的這兩個方法。有了這個直觀的感受我們接著進一步探索類對象的創(chuàng)建過程。
一個類在聲明了元類之后(就是類名后面加個括號,里面寫著metaclass=XXX)。當(dāng)程序運行時,在class語句的末尾就會自動創(chuàng)建類對象。假設(shè)我們有一個demo類,聲明它的元類為Meta,那么在demo的class語句完結(jié)后緊接著執(zhí)行一句:
demo=Meta(name,bases,dict)
傳入三個參數(shù),第一個是demo的類名稱(字符串類型),第二個是demo類的父類元組,第三個是demo類的類字典。這時候就需要關(guān)注Meta了,Meta也是一個類,它是type的子類,同時type也是Meta的元類,就是說Meta類對象是type的實例。在type類中有一個__call__方法,這個方法是一個運算符重載方法,攔截type(xxx,xxx,xxx,...)這樣的調(diào)用。回到剛才的
demo=Meta(name,bases,dict)
由于Meta是type的實例,因此當(dāng)這樣的調(diào)用形式出現(xiàn)時必然會觸發(fā)type中的__call__方法。由于Meta是type的實例,因此在傳參的時候除了剛剛寫出的三個參數(shù)外還會自動傳入一個Meta自己,因此type的__call__方法實際上會接收到四個參數(shù)?,F(xiàn)在程序運行到了type的__call__方法中,在這個方法中的調(diào)用過程我們用這樣的一段偽代碼來展示:

正如同代碼中所展示的,在__call__方法中首先調(diào)用元類的__new__方法得到一個類對象,再把這個類對象傳入元類的__init__方法對這個類對象進行初始化最后再返回這個類對象,這也印證了最初我們的論斷。元類構(gòu)造一個類對象基本上就是這么一個過程。
一個例子
為了更加說明元類構(gòu)造類對象的過程,我從書上找了一個例子改了一下貼在這:

這個例子是為了說明元類構(gòu)建類對象時__call__方法的調(diào)用。首先梳理一下程序的結(jié)構(gòu):Eggs和Spam是兩個常規(guī)的類,其中Spam是Eggs的子類,Spam的元類為SubMeta,而SubMeta的元類又為SuperMeta。之所以要繞這么一下就是為了讓元類本身的構(gòu)造過程也暴露出來。
接下來分析一下程序的運行。首先要構(gòu)建Spam就要先構(gòu)建SubMeta。SubMeta的元類為SuperMeta,在SuperMeta中定義了__init__和__new__方法,這是定義元類中一個比較常規(guī)的做法就不說了,我們要關(guān)注一下SuperMeta中定義的__call__方法并關(guān)注這個方法執(zhí)行的時機,這很重要。首先,構(gòu)建SubMeta等于執(zhí)行這樣的語句:
SubMeta=SuperMeta(name,bases,dict)
但這是否意味著SuperMeta的__call__會執(zhí)行呢?答案是否定的,因為從原理上來說SuperMeta是type的實例,因此上面的調(diào)用會執(zhí)行type中的__call__方法而不是SuperMeta中的。接著,由于SuperMeta中重載了type的__new__和__init__方法,因此type類中的__call__會調(diào)用SuperMeta的這兩個方法,調(diào)用之后SubMeta類對象就構(gòu)建完成了,注意此時的SubMeta是SuperMeta的類實例,明白這點很重要。接下來就要構(gòu)建Spam類對象:
Spam=SubMeta(name,bases,dict)
由于SubMeta是SuperMeta的實例,因此上面代碼的調(diào)用會觸發(fā)SuperMeta的__call__方法,就是我們剛剛提到的那個。接著會調(diào)用SubMeta類中的__init__和__new__方法,如果SubMeta中這兩個方法找不到或者沒找全,程序就會順著繼承樹找type類中的對應(yīng)方法。
那么我們預(yù)測一下輸出吧,首先肯定是SuperMeta的__new__和__init__執(zhí)行,然后是SuperMeta的__call__執(zhí)行,接著是SubMeta的__new__和__init__執(zhí)行:

結(jié)果很顯然,印證了之前的預(yù)測。
在這里,我們還可以繼續(xù)思考一下。此處的Spam是SubMeta的實例,如果在SubMeta中定義一個__call__方法,那么當(dāng)Spam正常創(chuàng)建實例的時候會發(fā)生什么呢?這個問題就暗示了python創(chuàng)建實例背后的故事,其實這個過程和類對象的構(gòu)建過程非常相似甚至代碼都是一模一樣的,同樣也是觸發(fā)__call__方法,進而調(diào)用__new__分配內(nèi)存,然后調(diào)用__init__做一些初始化的工作。只不過和類對象不同的是類對象創(chuàng)建中__new__返回的是一個類,而實例創(chuàng)建中返回的是一個實例。之所以會有這樣的區(qū)別在于在__call__方法中__new__和__init__的調(diào)用是取決于__call__的第一個參數(shù),上面的例子中即為Spam,而Spam不是type的子類,因此Spam的__init__和__new__方法指向的是他自己或者是object的對應(yīng)方法,因此才出現(xiàn)了__new__方法返回結(jié)果不同的現(xiàn)象。
寫在最后
好啦,元類基本的東西差不多就是這樣了,至于具體應(yīng)用還是蠻多的比如給類動態(tài)添加方法,模擬實現(xiàn)java中接口的特性等等。這都需要自己慢慢探索實踐。