網(wǎng)上關(guān)于metaclass的文章很多药磺,內(nèi)容也相當全面生動。在這里用學(xué)習(xí)記錄的方式嘗試自己復(fù)述一遍舟舒,加深理解拉庶。如有錯漏,還請包涵指教魏蔗。
python中我們由類實例化得到對象砍的,在python中,類同樣也是一個對象莺治,它也是實例化的結(jié)果廓鞠。
直觀一點,像這樣:
metaclass()=class
class()=object
網(wǎng)上一些經(jīng)典的講解谣旁,從這個概念直接跳到了type的使用上床佳,接著就是一些應(yīng)用化的東西了。在這里我自己總結(jié)了一下python中創(chuàng)建一個類的流程砌们。為python中元類“是什么”到“能做什么”間架起一道橋梁搁进。
首先我們用type來創(chuàng)建一個類:
type(name,bases,attr)#其中name為類名,bases為一個tuple饼问,表示繼承關(guān)系影兽,attr為包含了類屬性的dict
我們將它用一個函數(shù)包起來:
def meta(name,bases,attr):
print(name,bases,attr)
return type(name,bases,attr)
在一個類中指定其metaclass為上面的meta函數(shù):
class A(metaclass=meta):
pass
運行一下,注意這里并沒有A()莱革,但仍然有print的結(jié)果。說明當使用class關(guān)鍵字時捐名,python會自動尋找metaclass(如果沒有指定則使用默認的type),并為其傳入三個參數(shù)闹击。雖然概念上講镶蹋,能實例化得到一個類的,都能稱為元類赏半。這是廣義上的元類。
但python中從元類-->類-->實例,里面有些步驟是由python自動完成的琼腔,不遵循它的規(guī)則,要么報錯光坝,要么最后沒有想要的結(jié)果。因此在python中元類的使用基本上基于type(即python中的默認元類)及其子類盯另。
回到type上來,前面我們用type(name商蕴,bases芝发,attr)生成了類,但千萬不要將其認為是一個函數(shù)格郁。拋開元類的概念独悴,把它當成一個類一樣使用。用一個類繼承它并為其加入新的方法和屬性刻炒。
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é)果是一個類筏勒,并不是類的實例。同時meta('foo',(),{})上擁有類屬性bar厨埋、實例屬性hi捐顷、和實例方法hello(這里的類指元類,實例即元類的實例)废赞。實例方法hello是不是很像@staticmethod叮姑?從結(jié)果上看是這樣据悔,都獲得了一個類耘沼,在這個類在還未實例化時,就可以調(diào)用方法群嗤。究其原因狂秘,@staticmethod是一種功能上的實現(xiàn),而使用元類達到這種效果時赃绊,則是利用了“實例會帶有類中定義的屬性和方法,而類是元類的實例”這一語言的特性运敢。
值得注意的是:無論是元類的類屬性“foo”還是元類的實例屬性“hi”和實例方法“hello”忠售,在元類實例的實例(也就是我們最后得到的對象)中并不存在(這一點與下面會提到的在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()#注意這里多了一對括號浇坐,結(jié)果會報錯黔宛,因為元類實例的實例中沒有hello方法。
而如果使用@staticmethod觉渴,類實例化之后依然可以調(diào)用類方法:
class A:
@staticmethond
def hello():
print('hello')
A.hello()
A().hello()#兩者均可輸出“hello”
再看type徽惋,type(name,bases,attr)中第三個參數(shù)也能傳入方法和屬性,那么這些方法和屬性又會在哪里出現(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中第三個參數(shù)定義的屬性為類屬性而非實例屬性。
但是在其中傳入的方法則會變成實例方法,這里就不多演示了渺氧。
</br>
總結(jié)一下:
第一點,使用“class A(metaclass=用戶自定義的元類)”這樣的語句時白华,python會把classA中定義的屬性和方法傳入指定元類的第三個參數(shù)中贩耐。
第二點,元類中定義的屬性和方法管搪,雖然在元類的實例(類)中可以使用铡买,但在元類實例的實例(對象)中是沒有的。
由于第一點的存在澡为,在元類里添加?xùn)|西似乎對最終的對象沒什么影響景埃。
而第二點說明python會自動為你傳遞參數(shù),效果和使用type(name,bases,attr)沒什么區(qū)別拒啰,并且后者既麻煩也不直觀蒂胞。
因此元類雖然在對象生成鏈的上游,但并不能滿足“越靠近源頭越強大”的愿望骗随。
不過我們依然可以用元類做一些事鸿染,不難想到,我們可以攔截python自動傳給type的參數(shù)涨椒,動態(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”。這就是元類最基本的用法获三,讓我們改寫一下锨苏,不用函數(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__
徊都,雖然在例子中,它們的效果一樣主之,但在其他情況會出現(xiàn)一點小坑李根。
了解python中__new__
的朋友們知道:__new__
必須返回由其父類__new__
方法實例化的當前類,否則當前類的__init__
方法是不會被調(diào)用的粤攒。如果__new__
返回其他東西(包括但不限于其他類的實例囱持,幾乎可以是任何東西),類實例化的結(jié)果會直接指向__new__
的返回值盔几。具體看代碼:
class A:
def __new__(cls):
return 'I'm not a class'
print(A())#輸出“I'm not a class”
因此之前我們?nèi)绻褂玫氖莟ype掩幢,會出現(xiàn)“貍貓換太子”的情況上鞠⌒旧ィ看代碼:
class meta(type):
foo='foo'
def __new__(cls,name,bases,attr):
return type(name,bases,attr)
class A(metaclass=meta):
pass
print(A.foo)#會報錯缨恒,因為返回的是一個新的type實例。
這次我們換成type.__new__
來試試肿轨。
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)在最終的對象上掀序。我們是面向?qū)ο缶幊蹋詈蠓凑褂玫氖菍ο笠侗ⅲ?code>type還是type.__new__
换吧,似乎沒什么區(qū)別啊满着?
這里就要請出我們的__call__
方法了贯莺,坑也就是在這里出現(xiàn)的。
我們知道响驴,在類中定義__call__
方法撕蔼,會讓類如同函數(shù)一樣可以調(diào)用秽誊,那么在元類中定義__call__
會發(fā)生什么琳骡?
class meta(type):
def __call__(self):
print('hello')
class A(metaclass=meta):
foo='foo'
A()#輸出“hello”
print(A().foo)#報錯,顯示A()的類型為“NoneType”
可見如果我們在元類中定義__call__
方法最易,__call__
方法會覆蓋原來的__call__
,也就是實例化炫狱!
下面我們看一個用__call__
方法實現(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__()
,因為我們重寫了single的__call__
,它的實例已經(jīng)不能正確地返回原來的東西了(原來返回的是對象)。這時需要調(diào)用父類即type的__call__
方法鄙早,super()會自動幫我們找到繼承鏈的上一個類椅亚,并把當前的self作為參數(shù)傳入調(diào)用的方法中。父類的__call__
會返回正常的結(jié)果(這里的self是元類的實例——類弥虐,元類中定義的__call__
在元類實例化的結(jié)果——類被調(diào)用時生效媚赖,類中定義的__call__
在類實例化的結(jié)果——對象被調(diào)用時生效!)粥庄。
我們的單例依賴于重寫元類傳給類的__call__
方法豺妓,如果修改元類的__new__
方法,此時一定要記得返回super().__new__(cls,name,bases,attr)
或者type.__new__(cls,name,bases,attr)
!不要直接使用type(name,bases,attr)這種方式训堆,會出現(xiàn)“貍貓換太子”白嘁!生成的類中不存在__call__
。