寫在前面
這兩天仔細研究了python中元類的概念赡鲜,從最開始的一頭霧水桑逝,到現(xiàn)在的漸漸有一點明白赴背。想借這篇文章來闡述一下我對于python中元類的一些粗淺見解毛雇,同時也希望能給其他人一些啟發(fā)烹困,共同進步玄妈。首先,我想說在理解元類時一定要在大腦中時刻按照OOP的編程理念對程序進行分析才能夠理解元類中一些比較難以理解的地方髓梅,這也是筆者學習元類的一個小經驗拟蜻。
什么是元類
元類在python中最簡單的定義就是——type的子類。為了弄清楚這個模糊的定義枯饿,首先要弄清楚的是什么是type類酝锅,這對于后面的理解是非常重要的。type顧名思義就是“類型”的意思奢方,它和python標準庫中的其他類一樣也是一個類搔扁,但是它也有非常特殊的地方爸舒。其他的類的實例就是一個普通的實例,而類型的實例是一個類稿蹲。這是python3.0中一個更新的點扭勉,在2.X版本的python中類型的實例還不完全是一個類,在某些場景下它的實例也是一個普通的實例苛聘,不過今天我們要談的不是2.X版本中的type涂炎。讓我們用幾句簡單的代碼來驗證一下:
在代碼中首先構造了一個list實例,然后用type的構造函數(shù)以list的實例為參數(shù)構造了一個type的實例设哗,打印后我們發(fā)現(xiàn)type的實例是一個類唱捣,這和list實例的類(l.__class__)是相同的。這就印證了我們的一個論斷——類型的實例就是類熬拒。
熟悉OOP的人特別是C++的人都應該了解爷光,類只是聲明而不是實例垫竞,只有在程序中實例化一個類才會分配內存澎粟。但是python中類也是一個叫做類對象的對象,它不是憑空出現(xiàn)在程序中的欢瞪,它和實例一樣也需要有代碼對類對象進行實例化活烙,說穿了類對象應該也是一種特殊的實例,為什么這么說呢遣鼓?圖一中的代碼應該能給我們些許暗示:通過type的構造函數(shù)構造出類型的實例是一個類啸盏,或者說類對象(不能再細說了,再說就繞進去了骑祟,讀者自己意會吧)
那么回懦,寫到這里我們就可以說type類負責類對象的創(chuàng)建,一般情況下這種創(chuàng)建是隱式的不被我們發(fā)覺的次企。而如果我們要顯式地觀察這個過程就可以通過創(chuàng)建元類來實現(xiàn)怯晕。在OOP中攔截一個類方法的方式之一就是繼承這個類并且重載需要攔截的方法,那么這里就可以引出最開始給出的元類定義缸棵,元類就是type的子類舟茶。元類通過繼承type類,進而重載type類中的一些方法來達到控制類對象生成的目的堵第,這是元類編程中一個大體的思想吧凉。
類對象的創(chuàng)建過程
類對象的創(chuàng)建過程和實例的創(chuàng)建過程相似或者說大體上是一致的,我們通過一個例子來了解吧踏志。
打印效果如下:
可見在類對象創(chuàng)建時首先調用元類中的__new__方法阀捅,然后調用__init__方法。其中__new__方法返回類對象的實例针余,__init__方法對類對象進行一些初始化也搓。這兩個方法都是type類中的方法赏廓,在這里我們繼承type類后重載這兩個方法等于覆蓋了type類中的這兩個方法。有了這個直觀的感受我們接著進一步探索類對象的創(chuàng)建過程傍妒。
一個類在聲明了元類之后(就是類名后面加個括號幔摸,里面寫著metaclass=XXX)。當程序運行時颤练,在class語句的末尾就會自動創(chuàng)建類對象既忆。假設我們有一個demo類,聲明它的元類為Meta嗦玖,那么在demo的class語句完結后緊接著執(zhí)行一句:
demo=Meta(name,bases,dict)
傳入三個參數(shù)患雇,第一個是demo的類名稱(字符串類型),第二個是demo類的父類元組宇挫,第三個是demo類的類字典苛吱。這時候就需要關注Meta了,Meta也是一個類器瘪,它是type的子類翠储,同時type也是Meta的元類,就是說Meta類對象是type的實例橡疼。在type類中有一個__call__方法援所,這個方法是一個運算符重載方法,攔截type(xxx,xxx,xxx,...)這樣的調用欣除∽∈茫回到剛才的
demo=Meta(name,bases,dict)
由于Meta是type的實例,因此當這樣的調用形式出現(xiàn)時必然會觸發(fā)type中的__call__方法历帚。由于Meta是type的實例滔岳,因此在傳參的時候除了剛剛寫出的三個參數(shù)外還會自動傳入一個Meta自己,因此type的__call__方法實際上會接收到四個參數(shù)⊥炖危現(xiàn)在程序運行到了type的__call__方法中谱煤,在這個方法中的調用過程我們用這樣的一段偽代碼來展示:
正如同代碼中所展示的,在__call__方法中首先調用元類的__new__方法得到一個類對象卓研,再把這個類對象傳入元類的__init__方法對這個類對象進行初始化最后再返回這個類對象趴俘,這也印證了最初我們的論斷。元類構造一個類對象基本上就是這么一個過程奏赘。
一個例子
為了更加說明元類構造類對象的過程寥闪,我從書上找了一個例子改了一下貼在這:
這個例子是為了說明元類構建類對象時__call__方法的調用。首先梳理一下程序的結構:Eggs和Spam是兩個常規(guī)的類磨淌,其中Spam是Eggs的子類疲憋,Spam的元類為SubMeta,而SubMeta的元類又為SuperMeta梁只。之所以要繞這么一下就是為了讓元類本身的構造過程也暴露出來缚柳。
接下來分析一下程序的運行埃脏。首先要構建Spam就要先構建SubMeta。SubMeta的元類為SuperMeta秋忙,在SuperMeta中定義了__init__和__new__方法彩掐,這是定義元類中一個比較常規(guī)的做法就不說了,我們要關注一下SuperMeta中定義的__call__方法并關注這個方法執(zhí)行的時機灰追,這很重要堵幽。首先,構建SubMeta等于執(zhí)行這樣的語句:
SubMeta=SuperMeta(name,bases,dict)
但這是否意味著SuperMeta的__call__會執(zhí)行呢弹澎?答案是否定的朴下,因為從原理上來說SuperMeta是type的實例,因此上面的調用會執(zhí)行type中的__call__方法而不是SuperMeta中的苦蒿。接著殴胧,由于SuperMeta中重載了type的__new__和__init__方法,因此type類中的__call__會調用SuperMeta的這兩個方法佩迟,調用之后SubMeta類對象就構建完成了团滥,注意此時的SubMeta是SuperMeta的類實例,明白這點很重要音五。接下來就要構建Spam類對象:
Spam=SubMeta(name,bases,dict)
由于SubMeta是SuperMeta的實例惫撰,因此上面代碼的調用會觸發(fā)SuperMeta的__call__方法羔沙,就是我們剛剛提到的那個躺涝。接著會調用SubMeta類中的__init__和__new__方法,如果SubMeta中這兩個方法找不到或者沒找全扼雏,程序就會順著繼承樹找type類中的對應方法坚嗜。
那么我們預測一下輸出吧,首先肯定是SuperMeta的__new__和__init__執(zhí)行诗充,然后是SuperMeta的__call__執(zhí)行苍蔬,接著是SubMeta的__new__和__init__執(zhí)行:
結果很顯然,印證了之前的預測蝴蜓。
在這里碟绑,我們還可以繼續(xù)思考一下。此處的Spam是SubMeta的實例茎匠,如果在SubMeta中定義一個__call__方法格仲,那么當Spam正常創(chuàng)建實例的時候會發(fā)生什么呢?這個問題就暗示了python創(chuàng)建實例背后的故事诵冒,其實這個過程和類對象的構建過程非常相似甚至代碼都是一模一樣的凯肋,同樣也是觸發(fā)__call__方法,進而調用__new__分配內存汽馋,然后調用__init__做一些初始化的工作侮东。只不過和類對象不同的是類對象創(chuàng)建中__new__返回的是一個類圈盔,而實例創(chuàng)建中返回的是一個實例。之所以會有這樣的區(qū)別在于在__call__方法中__new__和__init__的調用是取決于__call__的第一個參數(shù)悄雅,上面的例子中即為Spam驱敲,而Spam不是type的子類,因此Spam的__init__和__new__方法指向的是他自己或者是object的對應方法宽闲,因此才出現(xiàn)了__new__方法返回結果不同的現(xiàn)象癌佩。
寫在最后
好啦,元類基本的東西差不多就是這樣了便锨,至于具體應用還是蠻多的比如給類動態(tài)添加方法围辙,模擬實現(xiàn)java中接口的特性等等。這都需要自己慢慢探索實踐放案。