python關(guān)于元類(mateclass)的理解和測(cè)試驗(yàn)證

參考:https://www.toutiao.com/i6481954430865375757/
https://www.keakon.net/2009/04/12/Python%E7%9A%84metaclass
類是現(xiàn)實(shí)世界或思維世界中的實(shí)體在計(jì)算機(jī)中的反映,它將數(shù)據(jù)以及這些數(shù)據(jù)上的操作封裝在一起。而對(duì)于python,經(jīng)常會(huì)聽(tīng)到,在Python中,一切皆對(duì)象,對(duì)象是類的實(shí)例化體現(xiàn)。那么類是怎么來(lái)的呢?

在python中,經(jīng)常見(jiàn)到聲明一個(gè)類用class Hello(object)種方式,python解釋器遇到class Hello(object)這句,是怎么處理的呢?object這個(gè)類又是怎么來(lái)的呢?

首先類也是一個(gè)對(duì)象, 這個(gè)對(duì)象由哪個(gè)類實(shí)例化的呢? 答案就是元類,通常就是type 或者繼承type的類。所以需要?jiǎng)?chuàng)建一個(gè)類的時(shí)候,我們可以有以下三種方式:

第一種:
class TestClass(object): # 當(dāng)python解釋器解釋完這句的時(shí)候,就已經(jīng)幫我們生成了一個(gè)TestClass類
    def __new__(cls, *args, **kwargs): #這個(gè) __new__是在實(shí)例化TestClass類的時(shí)候負(fù)責(zé)在內(nèi)存中創(chuàng)建TestClass類對(duì)象
        return super(TestClass, cls).__new__(cls, *args, **kwargs)
    def __init__(self, *args, **kwargs): #這個(gè)__init__是在實(shí)例化TestClass類的時(shí)候負(fù)責(zé)初始化__new__創(chuàng)建好的對(duì)象
        self.args = args
        self.kwargs = kwargs

第二種:
TestClass=type("TestClass", (object,), {})

第三種,也是對(duì)第一種的定制和改進(jìn):
class MyMetaClass(type):
    def __new__(cls, name, base, attr):
        return super(MyMetaClass, cls).__new__(cls, name, base, attr)
    def __init__(self, name, base, attr): #可要可不要
        super(MyMetaClass, cls).__init__(name, base, attr)
class TestClass(object):
    __metaclass__ = MyMetaClass    

先來(lái)猜測(cè)性的解釋一下第一種方式,也就是只用class TestClass(object)去聲明。當(dāng)python 解釋器看到class的聲明時(shí), 會(huì)先收集class語(yǔ)句范圍類聲明的變量,以k-v的形式放到字典attr中, 然后尋找metaclass, 因?yàn)門estClass中沒(méi)有定義,就去父類object中找,也沒(méi)有,最后就直接調(diào)用type, 將"TestClass", (object,) , attr三個(gè)參數(shù)傳給type生成TestClass對(duì)象。

然后我們?cè)谡{(diào)用TestClass類實(shí)例化對(duì)象時(shí),通常的調(diào)用方式就是TestClass("參數(shù)"), 上面我們已經(jīng)說(shuō)了,TestClass已經(jīng)是一個(gè)對(duì)象了,這里應(yīng)該想到__call__魔法函數(shù),TestClass("參數(shù)")也就是調(diào)用TestClass對(duì)象的__call__函數(shù), 但是我們并沒(méi)有定義__call__函數(shù),這個(gè)__call__函數(shù)從哪兒來(lái)呢。研究過(guò)python的MRO的話,應(yīng)該比較清楚,自身如果沒(méi)有某個(gè)函數(shù)或者方法,就會(huì)去找父類,或者兄弟類。 所以這里就回去找object中的__call__方法,而object中的__call__方法會(huì)做什么呢, 會(huì)去調(diào)用TestClass中定義的__new__和__init__方法(需要去看python源碼),這就到了我們看的見(jiàn)的流程了。通常我們并沒(méi)有定義__new__方法,所以 還是按照MRO,最后調(diào)用的父類的__new__,通常就是type的__new__或者object的__new__方法。

調(diào)用元類"__call__"函數(shù)這個(gè)點(diǎn),也可以通過(guò)元類的單例模式的實(shí)現(xiàn)得到驗(yàn)證,因?yàn)閱卫J骄褪且粋€(gè)類只能有一個(gè)實(shí)例,他的實(shí)現(xiàn)就是重寫元類的"__call__方法",實(shí)現(xiàn)如下:

class MyMetaClass(type):
    def __init__(cls,name, base, attr):
        super(MymetaClass, cls).__init__(name, base, attr)
        cls._instance = None
    def __call__(cls, *args, **kwargs):
        if cls._instance is None:
             cls._instance = super(MyMetaClass, cls).__call__(*args, **kargs)
        return cls._instance

class A():
    __metaclass__ = MyMetaClass

這樣從類的創(chuàng)建,到類的實(shí)例化過(guò)程就完了。這中間python為我們做了很多事情,很多就是python的黑魔法。 當(dāng)我們想自己定制一個(gè)類,這個(gè)類和其他很多類一樣,包含很多共同的特性的時(shí)候, 我們就可以用到元類了(當(dāng)然也可以用基類),也就是第三種方法。

第三種聲明類的方式的時(shí)候,因?yàn)門estClass中聲明了__metaclass__ ,所以會(huì)去調(diào)用MyMetaClass的__new__和__init__去創(chuàng)建TestClass這個(gè)類對(duì)象向,這個(gè)時(shí)候,我們就可以在__new__和__init__中做很多處理,這也就是__metaclass__ 存在的一個(gè)很重要的意義。 這里需要明確的就是MyMetaClass中的__new__和__init__的參數(shù),name 就是"TestClass", base就是(object,),attr就是一個(gè)字典,存放的是收集TestClass的屬性,比如__new__和__init__, 然后返回一個(gè)TestClass類。

看下面的例子

#!/usr/bin/env python
#-*- coding: utf-8 -*-

class MyMetaClass(type):
    def __new__(cls, name, base, attr):
        print ""
        print "mateclass new"
        print name
        print base
        print attr
        attr.update({"age": 27, "six": "boy"}) # 測(cè)試1
        print "id attr['__new__']: ", id(attr['__new__'])

        # attr = {"age": 27, "six": "boy"} # 測(cè)試2

        # attr = {"age": 27, "six": "boy", "__new__": attr['__new__'], "__init__": attr["__init__"]} # 測(cè)試3
        a = super(MyMetaClass, cls).__new__(cls, name, base, attr)
        print type(a), "   xxxxx"
        return a

    def __init__(self, name, base, attr):
        print ""
        print "mateclass init"
        print name
        print base
        print attr

    # def __call__(cls, *args, **kwds):
    #     print '__call__ of ', str(cls)
    #     print '__call__ *args=', str(args)
    #     return type(cls, *args, **kwds)

class TestObj(object):

    __metaclass__ = MyMetaClass

    A = "xxx"
    B = "YYY"
    C = "xxxx"

    def __new__(cls, *args, **kwargs):
        print ""
        print "TestObj new"
        print cls
        print args
        print kwargs
        return super(TestObj, cls).__new__(cls, *args, **kwargs)

    def __init__(self, x, y, z):
        print "TestObj init"
        self.x = x
        self.y = y
        self.z = z

print "id(__new__): ", id(TestObj.__new__)

if __name__ == "__main__":
    pass
    # a = TestObj(1, 2, 3)
    # print a.age

上面的代碼的運(yùn)行結(jié)果是

mateclass new
TestObj
(<type 'object'>,)
{'A': 'xxx', '__module__': '__main__', 'B': 'YYY', '__metaclass__': <class '__main__.MyMetaClass'>, '__new__': <function __new__ at 0x7f68fdddd668>, 'C': 'xxxx', '__init__': <function __init__ at 0x7f68fdddd6e0>}
id attr['__new__']:  140088912500328
<class '__main__.MyMetaClass'>    xxxxx
mateclass init
TestObj
(<type 'object'>,)
{'A': 'xxx', '__module__': '__main__', 'B': 'YYY', '__metaclass__': <class '__main__.MyMetaClass'>, '__new__': <function __new__ at 0x7f68fdddd668>, 'age': 27, 'six': 'boy', 'C': 'xxxx', '__init__': <function __init__ at 0x7f68fdddd6e0>}
id(__new__):  140088912500328

從結(jié)果中我們可以看出,MyMetaClass類中的中的 attr['__new__']的id是 140088912500328,和直接print "id(__new__): ", id(TestObj.__new__)的id 是一樣的,就驗(yàn)證了上面對(duì)執(zhí)行流程的猜測(cè)。同時(shí)在if __name__ == "__main__":語(yǔ)句塊中,我們沒(méi)有做任何操作,為什么還會(huì)調(diào)用mateclass new和mateclass init呢, 答案就是python對(duì)class MyMetaClass解釋時(shí), 調(diào)用了元類MyMetaClass生成了MyMetaClass類。

當(dāng)把if __name__ == "__main__":語(yǔ)句塊中的注釋取消掉,就會(huì)發(fā)現(xiàn)多了如下輸出:

TestObj new
<class '__main__.TestObj'>
(1, 2, 3)
{}
TestObj init
27

age明明是TestObj這個(gè)類的,為什么a能直接調(diào)用呢?(回想一下上面的創(chuàng)建對(duì)象的函數(shù)調(diào)用流程,元類的__call__函數(shù)去調(diào)用類定義的__new__和__init__,那調(diào)用時(shí)傳遞的參數(shù)從哪兒來(lái)?應(yīng)該就是元類收集類定義的數(shù)據(jù),然后放到的attr中,最后調(diào)用__call__的時(shí)候同樣用了attr)。
試想一下,A, B, C這三個(gè)變量是不是也是類TestObj的,我們也會(huì)在代碼中經(jīng)常通過(guò)實(shí)例比如a直接去訪問(wèn)。 所以在元類MyMetaClass中為TestObj 類新增的屬性age, six就是metaclass的一個(gè)小小的作用,只要其他類都想TestObj類一樣通過(guò)__metaclass__ = MyMetaClass 或者class TestObj(obj, metaclass=MyMetaClass)這種方式去引用了MyMetaClass,那么生成的類和類的實(shí)例都會(huì)有age, six屬性。

測(cè)試過(guò)程中,還發(fā)現(xiàn),如果直接向測(cè)試2的方式直接需修改attr,會(huì)出現(xiàn)如下錯(cuò)誤

Traceback (most recent call last):
  File "/home/xxxx/work/test/python_magic_test.py", line 58, in <module>
    a = TestObj(1, 2, 3)
TypeError: object() takes no parameters

對(duì)于這個(gè)問(wèn)題懵逼了很久, 請(qǐng)教大神后才發(fā)現(xiàn), 對(duì)于測(cè)試2(attr = {"age": 27, "six": "boy"} ),直接改變attr,導(dǎo)致attr收集的很多屬性丟失了,比如TestObj中定義的__new__和__init__方法,導(dǎo)致MyMetaClass 通過(guò)a = super(MyMetaClass, cls).__new__(cls, name, base, attr)創(chuàng)建的TestObj類沒(méi)有__new__,和__init__方法。 所以在執(zhí)行a = TestObj(1, 2, 3)的時(shí)候,按照MRO先去找TestObj這個(gè)類對(duì)象的"__call__"方法,但是肯定找不到,我們沒(méi)有顯示的定義,最后調(diào)用了父類object的"__call__",然后"__call__"調(diào)用__new__和__init__方法。這兩個(gè)方法雖然TestObj中定義了,但是在元類生成TestObj類的時(shí)候被去掉了,所以也就是沒(méi)有了,那就繼續(xù)按照MRO去找咯,也就是找到父類object類,然后object中的__init__方法不需要參數(shù),而TestObj(1, 2, 3)傳遞了三個(gè),所以就報(bào)錯(cuò)啦。具體代碼層面的證明,需要閱讀python的源碼,python的諸多黑魔法,就是python解釋器按照特定的語(yǔ)法規(guī)則為使用者悄悄的做了很多處理,初期使用起來(lái),看著很簡(jiǎn)單,很高效,但是越到后面,那些引以為傲的優(yōu)勢(shì),可能就成為你對(duì)python認(rèn)知和學(xué)習(xí)的障礙。

總結(jié)下來(lái):
1.python的類也是對(duì)象,這個(gè)對(duì)象是元類創(chuàng)建的
2.元類可以控制類的生成過(guò)程,從而定制一批特殊的類

最后編輯于
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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