Python中metaclass學(xué)習(xí)記錄

網(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__

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鲁沥,一起剝皮案震驚了整個濱河市画恰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌允扇,老刑警劉巖考润,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異唱矛,居然都是意外死亡俊戳,警方通過查閱死者的電腦和手機馆匿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門渐北,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人恃锉,你說我怎么就攤上這事呕臂。” “怎么了土砂?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵谜洽,是天一觀的道長阐虚。 經(jīng)常有香客問我,道長实束,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任诫龙,我火速辦了婚禮鲫咽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘锦聊。我一直安慰自己箩绍,他們只是感情好,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布圆到。 她就那樣靜靜地躺著卑吭,像睡著了一般豆赏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上掷邦,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天抚岗,我揣著相機與錄音,去河邊找鬼廷痘。 笑死件已,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的兄猩。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼鸠姨,長吁一口氣:“原來是場噩夢啊……” “哼淹真!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起巍糯,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤祟峦,失蹤者是張志新(化名)和其女友劉穎徙鱼,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體厌衙,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡绞绒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年处铛,在試婚紗的時候發(fā)現(xiàn)自己被綠了拐揭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡家肯,死狀恐怖讨衣,靈堂內(nèi)的尸體忽然破棺而出式镐,到底是詐尸還是另有隱情,我是刑警寧澤娘汞,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站燎孟,受9級特大地震影響尸昧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜爆侣,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一衷蜓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧斋陪,春花似錦置吓、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽告匠。三九已至,卻和暖如春后专,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背裸诽。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工型凳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留甘畅,地道東北人实夹。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓粒梦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親缴淋。 傳聞我的和親對象是個殘疾皇子泄朴,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理祖灰,服務(wù)發(fā)現(xiàn),斷路器局扶,智...
    卡卡羅2017閱讀 134,628評論 18 139
  • 寫在前面 這兩天仔細研究了python中元類的概念恨统,從最開始的一頭霧水,到現(xiàn)在的漸漸有一點明白三妈。想借這篇文章來闡述...
    光的文明閱讀 436評論 0 0
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法畜埋,類相關(guān)的語法,內(nèi)部類的語法畴蒲,繼承相關(guān)的語法悠鞍,異常的語法,線程的語...
    子非魚_t_閱讀 31,597評論 18 399
  • python的函數(shù)參數(shù)傳遞 看兩個例子: 所有變量都可以理解為內(nèi)存中一個對象的“引用”模燥,或者咖祭,可以看做C中的vio...
    marvinxu閱讀 5,838評論 2 30
  • 互聯(lián)網(wǎng)時代,知識的獲取從未如現(xiàn)在這般簡單蔫骂。而量的暴增么翰,并不等同于質(zhì)的提升纠吴。在當前信息過載的情況下硬鞍,如何在龐雜的慧瘤、魚...
    xihe11閱讀 889評論 1 0