網(wǎng)上關(guān)于metaclass的文章很多,內(nèi)容也相當(dāng)全面生動(dòng)。在這里用學(xué)習(xí)記錄的方式嘗試自己復(fù)述一遍,加深理解。如有錯(cuò)漏,還請(qǐng)包涵指教。
python中我們由類實(shí)例化得到對(duì)象,在python中,類同樣也是一個(gè)對(duì)象,它也是實(shí)例化的結(jié)果。
直觀一點(diǎn),像這樣:
metaclass()=class
class()=object
網(wǎng)上一些經(jīng)典的講解,從這個(gè)概念直接跳到了type的使用上,接著就是一些應(yīng)用化的東西了。在這里我自己總結(jié)了一下python中創(chuàng)建一個(gè)類的流程。為python中元類“是什么”到“能做什么”間架起一道橋梁。
首先我們用type來(lái)創(chuàng)建一個(gè)類:
type(name,bases,attr)#其中name為類名,bases為一個(gè)tuple,表示繼承關(guān)系,attr為包含了類屬性的dict
我們將它用一個(gè)函數(shù)包起來(lái):
def meta(name,bases,attr):
print(name,bases,attr)
return type(name,bases,attr)
在一個(gè)類中指定其metaclass為上面的meta函數(shù):
class A(metaclass=meta):
pass
運(yùn)行一下,注意這里并沒(méi)有A(),但仍然有print的結(jié)果。說(shuō)明當(dāng)使用class關(guān)鍵字時(shí),python會(huì)自動(dòng)尋找metaclass(如果沒(méi)有指定則使用默認(rèn)的type),并為其傳入三個(gè)參數(shù)。雖然概念上講,能實(shí)例化得到一個(gè)類的,都能稱為元類。這是廣義上的元類。
但python中從元類-->類-->實(shí)例,里面有些步驟是由python自動(dòng)完成的,不遵循它的規(guī)則,要么報(bào)錯(cuò),要么最后沒(méi)有想要的結(jié)果。因此在python中元類的使用基本上基于type(即python中的默認(rèn)元類)及其子類。
回到type上來(lái),前面我們用type(name,bases,attr)生成了類,但千萬(wàn)不要將其認(rèn)為是一個(gè)函數(shù)。拋開(kāi)元類的概念,把它當(dāng)成一個(gè)類一樣使用。用一個(gè)類繼承它并為其加入新的方法和屬性。
class meta(type):
foo='foo'
def __init__(self,name,bases,attr):#繼承自type類,初始化參數(shù)也和type一樣。
self.hi='hello'
def hello(self):
print(self.hi)
meta('foo',(),{}).hello()#輸出“hello”
注意這里的meta('foo',(),{})結(jié)果是一個(gè)類,并不是類的實(shí)例。同時(shí)meta('foo',(),{})上擁有類屬性bar、實(shí)例屬性hi、和實(shí)例方法hello(這里的類指元類,實(shí)例即元類的實(shí)例)。實(shí)例方法hello是不是很像@staticmethod?從結(jié)果上看是這樣,都獲得了一個(gè)類,在這個(gè)類在還未實(shí)例化時(shí),就可以調(diào)用方法。究其原因,@staticmethod是一種功能上的實(shí)現(xiàn),而使用元類達(dá)到這種效果時(shí),則是利用了“實(shí)例會(huì)帶有類中定義的屬性和方法,而類是元類的實(shí)例”這一語(yǔ)言的特性。
值得注意的是:無(wú)論是元類的類屬性“foo”還是元類的實(shí)例屬性“hi”和實(shí)例方法“hello”,在元類實(shí)例的實(shí)例(也就是我們最后得到的對(duì)象)中并不存在(這一點(diǎn)與下面會(huì)提到的在type參數(shù)內(nèi)傳入屬性或方法,得到的效果不一樣)。具體看代碼:
class meta(type):
bar='bar'
def __init__(self,name,bases,attr):#繼承自type類,初始化參數(shù)也和type一樣。
self.hi='hello'
def hello(self):
print(self.hi)
meta('foo',(),{})().hello()#注意這里多了一對(duì)括號(hào),結(jié)果會(huì)報(bào)錯(cuò),因?yàn)樵悓?shí)例的實(shí)例中沒(méi)有hello方法。
而如果使用@staticmethod,類實(shí)例化之后依然可以調(diào)用類方法:
class A:
@staticmethond
def hello():
print('hello')
A.hello()
A().hello()#兩者均可輸出“hello”
再看type,type(name,bases,attr)中第三個(gè)參數(shù)也能傳入方法和屬性,那么這些方法和屬性又會(huì)在哪里出現(xiàn)?
meta=type('meta',(),{'foo':'foo'})
A=meta()
B=meta()
print(A.foo,B.foo)#輸出“foo foo”
meta.foo='bar'
print(A.foo,B.foo)#輸出“bar bar”
上面表明了在type中第三個(gè)參數(shù)定義的屬性為類屬性而非實(shí)例屬性。
但是在其中傳入的方法則會(huì)變成實(shí)例方法,這里就不多演示了。
</br>
總結(jié)一下:
第一點(diǎn),使用“class A(metaclass=用戶自定義的元類)”這樣的語(yǔ)句時(shí),python會(huì)把classA中定義的屬性和方法傳入指定元類的第三個(gè)參數(shù)中。
第二點(diǎn),元類中定義的屬性和方法,雖然在元類的實(shí)例(類)中可以使用,但在元類實(shí)例的實(shí)例(對(duì)象)中是沒(méi)有的。
由于第一點(diǎn)的存在,在元類里添加?xùn)|西似乎對(duì)最終的對(duì)象沒(méi)什么影響。
而第二點(diǎn)說(shuō)明python會(huì)自動(dòng)為你傳遞參數(shù),效果和使用type(name,bases,attr)沒(méi)什么區(qū)別,并且后者既麻煩也不直觀。
因此元類雖然在對(duì)象生成鏈的上游,但并不能滿足“越靠近源頭越強(qiáng)大”的愿望。
不過(guò)我們依然可以用元類做一些事,不難想到,我們可以攔截python自動(dòng)傳給type的參數(shù),動(dòng)態(tài)地為其添加一些東西。
def meta(name,bases,attr):
attr['hello']='hello'
return type(name,bases,attr)
class A(metaclass=meta):
pass
print(A().hello)#輸出hello
果然,成功輸出了“hello”。這就是元類最基本的用法,讓我們改寫(xiě)一下,不用函數(shù)包裝,而是用類繼承的方式。
class meta(type):
def __new__(cls,name,bases,attr):
attr['hello']='hello'
return type(name,bases,attr)#注意這一行!
class A(metaclass=meta):
pass
print(A().hello)#輸出hello
看上面的注釋“#注意這一行”,這里我用的是type,而不是type.__new__,雖然在例子中,它們的效果一樣,但在其他情況會(huì)出現(xiàn)一點(diǎn)小坑。
了解python中__new__的朋友們知道:__new__必須返回由其父類__new__方法實(shí)例化的當(dāng)前類,否則當(dāng)前類的__init__方法是不會(huì)被調(diào)用的。如果__new__返回其他東西(包括但不限于其他類的實(shí)例,幾乎可以是任何東西),類實(shí)例化的結(jié)果會(huì)直接指向__new__的返回值。具體看代碼:
class A:
def __new__(cls):
return 'I'm not a class'
print(A())#輸出“I'm not a class”
因此之前我們?nèi)绻褂玫氖莟ype,會(huì)出現(xiàn)“貍貓換太子”的情況??创a:
class meta(type):
foo='foo'
def __new__(cls,name,bases,attr):
return type(name,bases,attr)
class A(metaclass=meta):
pass
print(A.foo)#會(huì)報(bào)錯(cuò),因?yàn)榉祷氐氖且粋€(gè)新的type實(shí)例。
這次我們換成type.__new__來(lái)試試。
class meta(type):
foo='foo'
def __new__(cls,name,bases,attr):
return type.__new__(cls,name,bases,attr)#注意換成了type.__new__
class A(metaclass=meta):
pass
print(A.foo)#正確返回!
欸,我們前面不是已經(jīng)有了結(jié)論,在元類里定義屬性和方法,都不出現(xiàn)在最終的對(duì)象上。我們是面向?qū)ο缶幊?,最后反正使用的是?duì)象,用type還是type.__new__,似乎沒(méi)什么區(qū)別???
這里就要請(qǐng)出我們的__call__方法了,坑也就是在這里出現(xiàn)的。
我們知道,在類中定義__call__方法,會(huì)讓類如同函數(shù)一樣可以調(diào)用,那么在元類中定義__call__會(huì)發(fā)生什么?
class meta(type):
def __call__(self):
print('hello')
class A(metaclass=meta):
foo='foo'
A()#輸出“hello”
print(A().foo)#報(bào)錯(cuò),顯示A()的類型為“NoneType”
可見(jiàn)如果我們?cè)谠愔卸x__call__方法,__call__方法會(huì)覆蓋原來(lái)的__call__,也就是實(shí)例化!
下面我們看一個(gè)用__call__方法實(shí)現(xiàn)的單例。
class single(type):
def __call__(self):
if not hasattr(self,'_instance'):
self._instance=super().__call__()#注意這里
return self._instance
class A(metaclass=single):
pass
注意其中的super().__call__(),因?yàn)槲覀冎貙?xiě)了single的__call__,它的實(shí)例已經(jīng)不能正確地返回原來(lái)的東西了(原來(lái)返回的是對(duì)象)。這時(shí)需要調(diào)用父類即type的__call__方法,super()會(huì)自動(dòng)幫我們找到繼承鏈的上一個(gè)類,并把當(dāng)前的self作為參數(shù)傳入調(diào)用的方法中。父類的__call__會(huì)返回正常的結(jié)果(這里的self是元類的實(shí)例——類,元類中定義的__call__在元類實(shí)例化的結(jié)果——類被調(diào)用時(shí)生效,類中定義的__call__在類實(shí)例化的結(jié)果——對(duì)象被調(diào)用時(shí)生效?。?。
我們的單例依賴于重寫(xiě)元類傳給類的__call__方法,如果修改元類的__new__方法,此時(shí)一定要記得返回super().__new__(cls,name,bases,attr)或者type.__new__(cls,name,bases,attr)!不要直接使用type(name,bases,attr)這種方式,會(huì)出現(xiàn)“貍貓換太子”!生成的類中不存在__call__。