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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,593評(píng)論 19 139
  • 寫(xiě)在前面 這兩天仔細(xì)研究了python中元類的概念,從最開(kāi)始的一頭霧水,到現(xiàn)在的漸漸有一點(diǎn)明白。想借這篇文章來(lái)闡述...
    光的文明閱讀 497評(píng)論 0 0
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法,類相關(guān)的語(yǔ)法,內(nèi)部類的語(yǔ)法,繼承相關(guān)的語(yǔ)法,異常的語(yǔ)法,線程的語(yǔ)...
    子非魚(yú)_t_閱讀 34,734評(píng)論 18 399
  • python的函數(shù)參數(shù)傳遞 看兩個(gè)例子: 所有變量都可以理解為內(nèi)存中一個(gè)對(duì)象的“引用”,或者,可以看做C中的vio...
    marvinxu閱讀 6,026評(píng)論 2 30
  • 互聯(lián)網(wǎng)時(shí)代,知識(shí)的獲取從未如現(xiàn)在這般簡(jiǎn)單。而量的暴增,并不等同于質(zhì)的提升。在當(dāng)前信息過(guò)載的情況下,如何在龐雜的、魚(yú)...
    xihe11閱讀 969評(píng)論 1 0

友情鏈接更多精彩內(nèi)容