[Python] 元類(Metaclasses )探索

Metaclasses are deeper magic that 99% of users should never worry about. If you wonder whether you need them, you don't (the people who actually need them know with certainty that they need them, and don't need an explanation about why). —— Python界的領袖 Tim Peters

Zen of Python的 作者Tim Peters大神說99%的Python用戶根本不需要為元類操心。雖然大神這么說了,但我認為還是有必要了解下這個Python的黑魔法,至少要知道它是什么東東。

關于元類的介紹網上已經有很多資料了,目前為止我認為介紹的最好的是《深刻理解Python中的元類(metaclass)》這篇從Stackoverflow翻譯過來的資料。
但是還有些瑕疵,比如__metaclass__如果定義在模塊頂層,那么只有對舊式類才會起作用,而它沒有對這點進行說明。所以如果可以話,請直接閱讀英文原文

讀完上面的文檔之后,我對于元類有了個大概的了解,但是其中的一些細節還是需要進一步實踐驗證,下面我就通過代碼來展示一些我對元類細節的理解。
注意:因為本人是使用Python 2.7為主,所以下文除了標明了Python 3.x的部分,其余都是基于Python 2.7的

元類的本質

元類就是用來創建類的“東西”,這句話必須要牢牢記住,元類是用來創建類的。我們知道類可以用來創建實例,而元類它的“實例”就是另一個類。

MyClass = MetaClass()
MyObject = MyClass()

__metaclass__

Python 2.7通過__metaclass__屬性來實現元類功能

def create_class(name, bases, attr): #注意必須有三個參數
    return type(name, bases, attr)

class MyClass(object):
    __metaclass__ = create_class

class MetaClass(type):
    pass

__metaclass__ = MetaClass
class MyClass1:
    pass

class MyClass2(object):
    __metaclass__ = MetaClass

在Python 2.7版本里__metaclass__屬性既可以是一個函數,也可以是一個類,只要這個東西在執行之后能返回一個類就行。
另外注意在模塊定義的__metaclass__屬性只對舊式類起作用

解釋器在生成類對象的時候會首選在當前類定義中查找是否有__metaclass__屬性,如果有則按照當前類定義的__metaclass__屬性生成類,否則繼續去父類中查找__metaclass__屬性,如果所有父類中都沒有找到__metaclass__屬性,則繼續在模塊中查找,如果還是沒有找到,則按照類定義生成類對象。(對于新式類,沒有查找模塊__metaclass__屬性這步)
所以__metaclass__屬性對解釋器而言就是一個生成類對象的Hook,它會攔截解釋器正常生成類對象的流程。

Python 3.x取消了__metaclass__屬性,改為

class MyClass3(metaclass=MetaClass): # MetaClass也可以是個函數
    pass

這種方式來實現元類。

元類構造過程

class MetaClass(type):
    
    def __init__(cls, name, bases, attr):
        print "MetaClass __init__:", cls.__name__
        
    def __new__(cls, name, bases, attr):
        print "MetaClass __new__:", cls.__name__
        new_cls =  super(MetaClass, cls).__new__(cls, name, bases, attr)
        print 'MetaClass create class:', new_cls.__name__
        return new_cls
        
    def __call__(cls, *args, **kwargs):
        print "MetaClass __call__:", cls.__name__
        return super(MetaClass, cls).__call__(cls, *args, **kwargs)

元類的定義和普通類相同,唯一的區別就是元類必須繼承type或者其他元類。
實例化過程也和普通類一樣,元類在實例化之前會調用__new__方法來生成一個新的類,這就是元類和普通類的最大區別,接著__new__方法會將新生成的類傳遞給__init__方法。和普通類一樣,如果__new__方法不返回一個類,__init__方法不會被調用。

class MyClass(object):
    __metaclass__ = MetaClass

    def __init__(self, *args, **kwargs):
        print "MyClass __init__"
  
    def __new__(cls, *args, **kwargs):
        print "MyClass __new__"
        return super(MyClass, cls).__new__(cls, *args, **kwargs)
    
    def __call__(self, *args, **kwargs):
        print "MyClass __call__"

代碼執行之后輸出

MetaClass __new__: MetaClass
MetaClass create class: MyClass
MetaClass __init__: MyClass

Python中一切都是對象,包括類,所以解釋器會在解析到類定義的時候,生成一個當前命名空間中唯一的一個類對象。而我在上面說過__metaclass__屬性會攔截生解釋器生成類對象的過程,上面的輸出就證明了這點。

__call__方法在生成類的過程中不會被調用,它會在元類生成的類生成實例之前被調用,有點繞,直接看代碼比較清晰。

# MyClass類定義之后加上下面的語句用來生成一個類實例
mc1 = MyClass()

代碼執行之后輸出

MetaClass __new__: MetaClass
MetaClass create class: MyClass
MetaClass __init__: MyClass
MetaClass __call__: MyClass
MyClass __new__
MyClass __init__

可以看到元類的__call__方法被調用了。
為什么呢?
首選來回顧下__call__方法會在什么情況下被調用?
類如果定義了__call__方法那么就表明類的實例也是一個可調用的對象,比如

class Person(object):
    def __init__(self, name):
        self.name = name

    def __call__(self):
        print "Hello", self.name
>>> p = Person('Tim')
>>> p()
Hello Tim

如果沒有__call__方法,類實例p無法被調用。

回到主題,MyClass可以被視作是元類MetaClass的實例,雖然它是一個類,又因為元類實現了__call__方法,所以元類的實例是可調用的。所以在執行MyClass()的時候,才會調用元類的__call__方法。
再說一句,元類繼承type之后,可以不用自己實現__call__方法,它的實例就是可調用的,這也是元類和普通類的一個區別。

元類__new__方法的參數

Python 2.7版本中元類的__new__方法會傳遞4個參數

def __new__(cls, name, bases, attr):
    print "MetaClass.__new__(cls=%s, name=%r, bases=%s, attrs=[%s])" % (cls, name, bases, ", ".join(attr))
    return super(MetaClass, cls).__new__(cls, name, bases, attr)

輸出

MetaClass.__new__(cls=<class '__main__.MetaClass'>, name='MyClass', bases=(<type 'object'>,), attrs=[__call__, __module__, __metaclass__, __new__, __init__])
MetaClass __init__: MyClass

cls:元類對象
name:要生成的類的類名
bases:要生成的類的所有父類,是個元組(tuple)
attr:要生成類的所有屬性,是個字典

在Python 3.x版本上__new__方法的參數有了變動,可以傳遞額外的參數了。

class MetaClass(type):
    def __new__(cls, name, bases, attr, **options):
        print('name=%s, bases=%s, attr=[%s], **%s' % (name, bases, ', '.join(attr), options))
        return super(MetaClass, cls).__new__(cls, name, bases, attr)

class MyClass(metaclass=MetaClass, extra=1):
    pass

輸出

name=MyClass, bases=(), attr=[__module__, __qualname__], **{'extra': 1}

可以看到extra作為一個關鍵字參數被傳遞給了__new__方法

總結

元類作為Python中的黑魔法,雖然用到的機會不多,但技多不壓身,多懂點總是沒有錯的。何況Django的ORM就用到了元類,了解元類可以更好的理解ORM代碼。推薦廖雪峰老師用元類實現ORM的教程——Day 3 - 編寫ORM,看了之后會對元類有更深的理解。

以上就是目前我對元類的全部理解,記于 2018-03-27

參考:
http://blog.jobbole.com/21351/
https://stackoverflow.com/a/6581949/9500863
https://blog.ionelmc.ro/2015/02/09/understanding-python-metaclasses/
http://martyalchin.com/2011/jan/20/class-level-keyword-arguments/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容