Python學習之路41-元類

VPointer發表於2018-08-24

《流暢的Python》筆記。

本篇主要討論Python中的元類。Python中所有的類都直接或間接地是元類type的例項。閱讀本篇時,請時刻注意你所閱讀的內容指的是**"例項"(或者說"物件")"類"還是"元類"**,否則很容易被繞暈。

1. 前言

Python中,幾乎所有的東西都是物件。不光類的例項是物件,連類本身也是物件

不像C++、Java等靜態語言,在編譯前就必須將類定義好,執行時不能再建立新類,Python則可以在執行時動態建立新類,且不通過關鍵字class建立類的類叫做元類元類也是類,它可以派生出新的元類,但所有元類最頂層的超類只有一個,就是我們經常用到的type。Python中所有的類都直接或間接地是type例項

在執行時能通過元類動態建立類是Python的魅力,但想要理解這個"魅力"確並不是那麼容易。本篇內容主要有:元類的簡單示例,類裝飾器,元類的原理、定義及使用方式,最後使用元類來彌補上一篇中描述符的不足。

本篇只能算是對元類的初步介紹,更深層次的內容還需進一步學習。

2. 初識元類

通常,如果要建立物件,需要先在某個模組中用class關鍵字定義好類,再在業務程式碼中建立這個類的例項。與這種事先定義的方式相反,可以通過type在執行時建立類,以下是它的示例:

>>> a = "this is a string"
>>> type(a)
<class 'str'>
>>> MyClass = type("MyClass", (object,), {"x": 1, "x2": lambda self: self.x * 2})
>>> mc = MyClass()
>>> mc.x
1
>>> mc.x2()
2                      # 請留意下方這三個特殊屬性
>>> mc.__class__       # __class__的值是例項所屬的類
<class 'MyClass'>
>>> MyClass.__bases__  # __bases__的值是類的所有直接超類
(<class 'object'>,)
>>> MyClass.__mro__    # __mro__的值是類的所有超類
(<class 'MyClass'>, <class 'object'>) 
>>> MyClass.__class__  # 這表明MyClass這個類是type的物件
<class 'type'>
複製程式碼

上述MyClass的定義等同於如下定義:

class MyClass(object):
    x = 1
    def x2(self):
        return self.x * 2
複製程式碼

type通常被當做函式使用,但它其實是一個類。當只傳入一個例項時,它返回例項的型別;當傳入3個引數時,它生成並返回一個類:

type(cls_name, bases, attr_dict)
複製程式碼

其中:

  • cls_name是要建立的類的名稱的字串;
  • bases是一個元組,它儲存即將建立的類的直接父類們,比如MyClass繼承自object(如果只繼承自object,可以將bases設定為空元組);
  • attr_dict是新類的屬性字典。不光包括資料屬性,還包括了方法(含特殊方法)。不過,如果是資料屬性,這些資料屬性將成為類屬性,而不是例項屬性。如果想建立例項屬性,請在attr_dict中傳入__init__的定義,或者傳入__dict__

為了更詳細的介紹type的用法,我們用它來構造一個類工廠函式。

3. 類工廠函式

對於資料結構固定的資料,如果想將其包裝成類物件,傳統的做法是使用class定義每個類,比如為寵物應用定義各種動物類:

class Dog:
    def __init__(self, name, weight, owner):
        self.name = name
        self.weight = weight
        self.owner = owner
複製程式碼

不知道各位在敲這段程式碼時有沒有抱怨:nameweightowner敲了三遍!如果再多幾種動物類,這種樣板程式碼得寫多少?當然,對於相關的類可以選擇繼承。但如果資料間不相關呢?難道定義每個類的時候都將這種樣板程式碼敲一遍?這個時候就可以用類工廠函式來減少這種樣板程式碼。下方程式碼展示了type更具體的用法,生成的類比較簡單,適合用於處理格式固定的資料。這個工廠函式其實是在模仿collections.namedtuple

def record_factory(cls_name, field_name):
    try:   # 假設傳入的field_name是字串,獲取屬性名
        field_names = field_name.replace(",", " ").split()
    except AttributeError:
        pass  # 如果不是字串,則當做可迭代物件處理
    field_names = tuple(field_names)   # 將屬性名存到元組中
    # __init__不作用於這個工廠函式!這是為要建立的類定義的構造方法
    def __init__(self, *args, **kwargs):        
        attrs = dict(zip(self.__slots__, args))
        attrs.update(kwargs)
        for name, value in attrs.items():
            setattr(self, name, value)

    def __iter__(self):   # 讓即將建立的類可迭代
        for name in self.__slots__:
            yield getattr(self, name)

    def __repr__(self):   # 格式化輸出
        values = ", ".join("{}={!r}".format(*i) for i in zip(self.__slots__, self))
        return "{}({})".format(self.__class__.__name__, values)
    # 類將擁有的屬性
    cls_attrs = dict(__slots__=field_names, __init__=__init__,
                     __iter__=__iter__, __repr__=__repr__)
    return type(cls_name, (), cls_attrs)   # 繼承自object
複製程式碼

下面是這個類工廠函式的用法:

>>> Dog = record_factory("Dog", "name weight owner")
>>> dog = Dog("test", 5, "Kevin")
>>> dog
Dog(name='test', weight=5, owner='Kevin')
>>> dog.weight = 6
>>> dog
Dog(name='test', weight=6, owner='Kevin')
>>> name, weight, owner = dog
>>> name, weight, owner
('test', 6, 'Kevin')
複製程式碼

下面我們將進一步瞭解元類。

4. 元類

物件導向的思想有兩大關係:類的繼承和類的例項化。在Python中,typeobject就像兩個緊密合作的管理員,type主管例項化,object主管繼承。

我們都知道,Python中所有的類都是從object繼承而來的。但如果你看過下方的程式碼後,不知道對這一點的理解會不會動搖:

>>> type.__bases__
(<class 'object'>,)
>>> type.__class__
<class 'type'>
>>> object.__bases__
()
>>> object.__class__
<class 'type'>
複製程式碼

這段程式碼翻譯成中文就是:objecttype的例項,typeobject的子類,因此type也是type自身的例項。這裡完美地扯出了一個"先有蛋還是先有雞"的問題:既然objecttype的例項,那就得先由type建立object;但type又是object的子類,也就是得先有object,再有type,所以到底是誰先有?

這個關係直到現在我也沒搞清楚。如果是現實世界,可以說人類暫時還沒搞清楚是先有雞還是先有蛋,但Python這種程式語言可是人造的東西,況且底層還是C語言,所以我肯定不信什麼"互為祖先,同時產生"這種說法,而且這在程式碼裡不是死迴圈嗎?查了很多資料,基本都是引用的這張圖,其中虛線表示例項關係,實線表示繼承關係:

Python學習之路41-元類

但大家都回避了前面那個問題:objecttype例項化而來,可type又從object派生而來,到底誰先存在?

只能去看原始碼。原始碼中type確實繼承自object,但object的定義中並沒有metaclass關鍵字出現(後面會講到,如果以class的形式從元類例項化類,需要使用這個關鍵字);並且,object中有這麼一個定義:

__class__ = None # (!) forward: type, real value is ''
複製程式碼

這就讓疑惑更深了:object究竟是不是type的例項?type中有如下定義:

    __bases__ = (
        object,
    )
    __base__ = object
    __mro__ = (
        None, # (!) forward: type, real value is ''
        object,
    )
複製程式碼

更深層的原始碼暫時還啃不動。官方說明文件中說明了類的構建過程:所有的類,不管指沒指明元類,都會經由type(),產生實際使用的類,這也驗證了所有的類都是type的例項這一說法。

這兩者的具體關係還有待繼續研究。但我們畢竟不是語言專家,我們更看重的是元類怎麼使用。對於這些概念,我們只需要知道:

  • 元類type可以建立類;
  • 所有的類都直接或間接的是type的例項;
  • type是自身的例項;
  • type可以被繼承,用於建立新的元類。

4.1 類裝飾器

在繼續元類之前,我們先來解決上一篇屬性描述符沒有解決的問題:儲存屬性需要手動指定,而自動生成的名稱所表達的意思又不夠明顯:

>>> Food.weight.storage_name
'_Quantity#0'
複製程式碼

這是上一篇文章中自動生成儲存屬性的名稱時採用的策略,但我們更希望是下面這種形式:

>>> Food.weight.storage_name
'_Quantity#weight'
複製程式碼

上一篇中也說過,描述符類很難獲取託管類的類屬性名稱的。使用類裝飾器則能解決這個問題。類裝飾器和函式裝飾器非常相似,是引數為類物件的函式,返回原來的類或修改後的類。這裡我們將它裝飾到Food類上,而不是Quantity類上(FoodQuantity的具體定義請檢視上一篇文章。以下程式碼不能直接執行,請自行匯入所需的類):

@entity
class Food:   # 這個類比上一篇有所省略
    weight = Quantity()   # 並沒有傳入儲存屬性的名稱
    def __init__(self, weight):
        self.weight = weight

def entity(cls):
    for key, attr in cls.__dict__.items():
        if isinstance(attr, Validated):      # 如果這個屬性是Validated類的例項
            type_name = type(attr).__name__  # 則修改它的storage_name屬性的值
            attr.storage_name = "_{}#{}".format(type_name, key)
    return cls
複製程式碼

其實實現的思路很簡單:Quantity之所以無法獲取Food類的屬性名,是因為在Food中生成Quantity例項時,Food這個類都還沒有建立完畢,自然只能手動傳入。那等Food類建立完畢了再設定值不就行了?與函式裝飾器類似,類裝飾器會在Food生成後立即執行。

也可以用類裝飾器來替換掉類中的某些方法。但類裝飾器有一個重大缺點:只能對直接依附的類有效。這意味著,被裝飾類的子類不一定繼承裝飾器所做的修改,具體情況視改動的方式而定。

小插曲:我看到這個概念的時候,無法理解為什麼這被稱之為"缺點":繼承的時候子類重寫了父類的同名方法,這不再正常不過嗎?難道是不準讓子類重寫這個方法,要讓這個方法在整個繼承體系中都保持唯一?那不重寫不就完了嗎?如果整個專案就一個人做,當然能保證不重寫這個方法。但往往軟體開發是個團隊專案,其他人並不一定清楚這個方法能不能被重寫。

要保持方法在整個繼承體系中保持唯一,不被子類所覆蓋,這就得祭出元類。

4.2 使用元類

當使用到元類的時候,其實就是在定製化類及其子類的行為。下面我們使用元類替換掉前面的類裝飾器:

# Validated和Quantity都在上一篇文章中,以下程式碼不能直接執行!
class EntityMeta(type):  # 在元類中,通常將self換成cls
    def __init__(cls, name, bases, attr_dict):  # 邏輯和類裝飾器是一樣的
        super().__init__(name, bases, attr_dict)# 這一步將name,bases,attr_dict繫結到了cls上
        for key, attr in attr_dict.items():
            if isinstance(attr, Validated):
                type_name = type(attr).__name__
                attr.storage_name = "_{}#{}".format(type_name, key)

class Entity(metaclass=EntityMeta):
    """帶有驗證欄位的業務實體"""   # 什麼都不用做,除非像新增新方法

class Food(Entity):    # 對這個類進行了簡化
    weight = Quantity()
    def __init__(self, weight):
        self.weight = weight
複製程式碼

請注意區分這些類的關係:

  • EntityMeta是元類type的子類,所以它也是個元類。
  • Entity使用class關鍵字來定義新的類,而不是呼叫type()函式來建立新的類;在定義Entity時,使用了metaclass關鍵字,表明這是元類EntityMeta的例項,而不是EntityMeta的子類,即,這不是繼承關係。同時,Entity也(間接)是type的例項。
  • FoodEntity的子類,也是元類EntityMetatype的例項。

列出這些關係,是想提醒大家,如果要自行定義元類,請時刻注意,究竟誰是誰的子類,誰是誰的例項。下面來執行一下這段程式碼:

>>> Food.weight.storage_name
'_Quantity#weight'   # 行為符合預期
複製程式碼

這裡又產生了3個問題。

第1個問題是:從EntityMeta__init__中可以看到,引數cls存的是元類的例項的引用,即類Entity或者類Food的引用,但整個初始化過程中,根本就沒有用到cls,可結果表明,這些修改確實作用到了Food上,那麼這是怎麼作用Food上的呢?

EntityMeta.__init__()這個方法中的語句並不多,簡答分析就能知道,問題出在super().__init__(),即type.__init__()上。但這個方法的具體實現我暫時也不知道,只能給出我的猜想:我們都知道,對於普通的類(例如Food)來說,它的物件(例如f)儲存在記憶體中的某個位置a上,Food__init__操作記憶體a上的資料;而開篇就提到,所有的類也都是物件,於是類比一下,元類(例如EntityMeta)的例項(例如Food)肯定也儲存在記憶體的某個位置b,那麼type.__init__肯定將傳入的引數關聯到了記憶體b(例項Food)上。所以,這些操作最後在Food上生效了。平時對類的使用,其實就是用記憶體b中的資料建立記憶體a中的資料。

元類之所以難理解,難就難在我們並不習慣於"根據類來建立類"這種思路,我們習慣的是"根據類來建立例項"。這裡也再次申明,沒有"根據類來建立類"這種說法,一切都是"根據類來建立例項"!當涉及到元類時,就把元類看做平常使用的類,把元類生成的類看做平常使用的例項(或者說物件)。如果能這樣想,下面兩個問題也就能很好回答:

  • 兩個__init__誰先執行呢?
  • 前面說到,在元類中定義的方法能影響到整個繼承體系,即使子類重寫這個方法也沒有用,那這是怎麼做到的呢?

要徹底回答這兩個問題,就要涉及到執行時匯入時的概念。

5. 執行時&匯入時

為了正確地做超程式設計,必須知道Python直譯器在什麼時候執行各個程式碼塊。Python程式設計師區分執行時匯入時這兩個概念,但其實這兩個術語並沒有嚴格定義,而且兩者有交集。

在匯入時,直譯器會編譯原始檔,直譯器會從上到下一次性解析完整個.py模組,然後生成用於執行的位元組碼,並將其存到.pyc檔案中(這類檔案在本地的__pycache__資料夾中)。所以,雖然Python是解釋型語言,邊解釋邊執行,但解釋的不是.py原始檔,而是.pyc中的位元組碼資料。

匯入時除了編譯,還會做些其他的事情。由於Python中的語句幾乎都是可執行的,稍不注意,某些本該在執行時才執行的語句會在匯入時就執行,導致使用者程式的狀態被修改。這裡指的就是import語句:

  • 在Java中,import只用作宣告,執行的時候才真正執行import後面的包中的程式碼。
  • 在Python中,import不僅僅是宣告。模組首次被匯入時,模組中所有的程式碼都會被執行並快取,以後再匯入相同的模組時直接使用快取(只做名稱繫結);所匯入的模組中如果還有import,那麼這些模組只要沒被匯入過,也會被執行一遍。這表明,執行時匯入時產生了交集。

之前我在網上搜尋這個概念的時候,很多博主都說,匯入時會執行模組中所有的程式碼。其實並不是這樣的,Python直譯器確實會將模組從頭執行到尾,但是:

  • 對於函式,直譯器只執行完def關鍵字所在的行,它會編譯函式的定義體,把函式物件繫結到對應的全域性名稱上,但顯然函式定義體不會被執行。只有到執行時,直譯器才通過全域性名稱找到函式定義體,再執行它。
  • 對於類,情況就不一樣了。匯入時,直譯器會執行每個類的定義體,甚至會執行巢狀類的定義體。這麼做的結果就是,定義了類的屬性和方法(方法的定義體依然不會被執行),並構建了類這個物件。

繞了這麼一大圈,終於和元類發生了關係!類這個物件在匯入時就會被建立,所以,元類在匯入時就會初始化它的例項:類。

為了更真切的體驗這個過程,下面建立幾個類,並觀察直譯器在匯入時和執行時的行為。

下面的程式碼會用到類裝飾器和元類,前面說到類裝飾器在子類中不一定起作用,但元類一定起作用,請留意這兩個的行為。

5.1 一般情況

這裡指沒有元類的情況。首先建立兩個模組,程式碼可能有點長,但都很簡單。注意這兩個模組的名稱,首先是evaltime.py

# evaltime.py
from evalsupport import deco_alpha

print('<[1]> evaltime module start')

class ClassOne:   # 它巢狀了一個類
    print('<[2]> ClassOne body')

    def __init__(self):
        print('<[3]> ClassOne.__init__')

    def __del__(self):
        print('<[4]> ClassOne.__del__')

    def method_x(self):
        print('<[5]> ClassOne.method_x')

    class ClassTwo(object):
        print('<[6]> ClassTwo body')

@deco_alpha
class ClassThree:  # 它被類裝飾器裝飾
    print('<[7]> ClassThree body')

    def method_y(self):  # 注意才場景2中觀察這個方法的行為
        print('<[8]> ClassThree.method_y')

class ClassFour(ClassThree):   # 這裡有一個繼承,ClassThree被類裝飾器裝飾過
    print('<[9]> ClassFour body')

    def method_y(self):  # 注意才場景2中觀察這個方法的行為
        print('<[10]> ClassFour.method_y')

if __name__ == '__main__':
    print('<[11]> ClassOne tests', 30 * '.')
    one = ClassOne()
    one.method_x()
    print('<[12]> ClassThree tests', 30 * '.')
    three = ClassThree()
    three.method_y()
    print('<[13]> ClassFour tests', 30 * '.')
    four = ClassFour()
    four.method_y()

print('<[14]> evaltime module end')
複製程式碼

接著是evalsupport.py

# evalsupport.py
print('<[100]> evalsupport module start')

def deco_alpha(cls):
    print('<[200]> deco_alpha')

    def inner_1(self):
        print('<[300]> deco_alpha:inner_1')

    cls.method_y = inner_1
    return cls

class MetaAleph(type):
    print('<[400]> MetaAleph body')

    def __init__(cls, name, bases, dic):
        print('<[500]> MetaAleph.__init__')

        def inner_2(self):
            print('<[600]> MetaAleph.__init__:inner_2')

        cls.method_z = inner_2    # 例項中的這個屬性如果有,則會被替換
                                  # 如果沒有,則新建這個屬性並賦值為內嵌函式inner_2的引用
print('<[700]> evalsupport module end')
複製程式碼

上面這兩個模組的程式碼中有<[N]>標記,N表示數字。現在請大家模擬以下兩種場景,記錄標記出現的順序,最後再和真實結果比較。

場景1:在Python控制檯中以互動的方式匯入evaltime.py模組,即

>>> import evaltime.py
複製程式碼

場景2:在命令列中執行evaltime.py模組,即

$ python3 evaltime.py
複製程式碼

建議模擬完後再看下面的結果:

# 場景1
>>> import evaltime.py
<[100]> evalsupport module start    # 執行evalsupport.py模組
<[400]> MetaAleph body              # MetaAleph的定義體執行了
<[700]> evalsupport module end      # 函式deco_alpha定義體在匯入時並沒有被執行!
<[1]> evaltime module start         # 開始執行evaltime.py模組
<[2]> ClassOne body                 # ClassOne的定義體被執行了,但其中的方法沒有被執行
<[6]> ClassTwo body                 # 巢狀的ClassTwo的定義體也被執行了
<[7]> ClassThree body               # ClassThree的定義體被執行。
<[200]> deco_alpha    # 跳到了類裝飾器中,函式定義體在匯入時被執行了!證明匯入時建立了類物件
<[9]> ClassFour body                # 類定義體被執行
<[14]> evaltime module end          # 模組執行完畢

# 場景2
$ python3 evaltime.py
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[1]> evaltime module start
<[2]> ClassOne body
<[6]> ClassTwo body
<[7]> ClassThree body
<[200]> deco_alpha
<[9]> ClassFour body                # 在此行之前,和匯入時沒有區別,畢竟要執行得先匯入嘛
<[11]> ClassOne tests ..............................   # 開始執行if中的內容了
<[3]> ClassOne.__init__             # 初始化ClassOne
<[5]> ClassOne.method_x             # 呼叫ClassOne的method_x方法
<[12]> ClassThree tests .............................. 
<[300]> deco_alpha:inner_1          # ClassThree的method_y被替換了
<[13]> ClassFour tests ..............................
<[10]> ClassFour.method_y           # 類裝飾器在子類上不起作用
<[14]> evaltime module end          # 模組執行結束
<[4]> ClassOne.__del__              # ClassOne在被銷燬時呼叫__del__方法
複製程式碼

不知大家的模擬是否和結果一致?

場景2中的結果證明了,類裝飾器在子類中不一定起作用。

兩個場景中,類裝飾器在匯入時都執行了一次,這證明了類物件在匯入時建立,而不是在執行時建立。

5.2 加入元類

還剩下的兩個問題將在這個例子中找到答案。不過,還得再建立一個模組evaltime_meta.py,並建議大家回顧一下MetaAleph的實現:

# evaltime_meta.py
from evalsupport import deco_alpha, MetaAleph

print('<[1]> evaltime_meta module start')

@deco_alpha
class ClassThree():  # 被類裝飾器裝飾
    print('<[2]> ClassThree body')

    def method_y(self):
        print('<[3]> ClassThree.method_y')

class ClassFour(ClassThree):
    print('<[4]> ClassFour body')

    def method_y(self):
        print('<[5]> ClassFour.method_y')

class ClassFive(metaclass=MetaAleph):   # 它是元類MetaAleph的例項!
    print('<[6]> ClassFive body')

    def __init__(self):
        print('<[7]> ClassFive.__init__')

    def method_z(self):
        print('<[8]> ClassFive.method_y')

class ClassSix(ClassFive):   # 它也是元類MetaAleph的例項!
    print('<[9]> ClassSix body')

    def method_z(self):
        print('<[10]> ClassSix.method_y')

if __name__ == '__main__':
    print('<[11]> ClassThree tests', 30 * '.')
    three = ClassThree()
    three.method_y()
    print('<[12]> ClassFour tests', 30 * '.')
    four = ClassFour()
    four.method_y()
    print('<[13]> ClassFive tests', 30 * '.')
    five = ClassFive()
    five.method_z()
    print('<[14]> ClassSix tests', 30 * '.')
    six = ClassSix()
    six.method_z()

print('<[15]> evaltime_meta module end')
複製程式碼

還是那兩個場景:

場景1:在Python控制檯中匯入evaltime_meta.py

>>> import evaltime_meta.py
複製程式碼

場景2:在命令列中執行evaltime_meta.py

$ python3 evaltime_meta.py
複製程式碼

以下是兩個場景的結果:

# 場景1
>>> import evaltime_meta.py
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[1]> evaltime_meta module start
<[2]> ClassThree body
<[200]> deco_alpha
<[4]> ClassFour body        # 到這裡為止,和上一個場景1的情況一樣
<[6]> ClassFive body        # 執行了ClassFive定義體
<[500]> MetaAleph.__init__  # 元類中的初始化方法在匯入時被執行了!也證明匯入時建立了類物件
<[9]> ClassSix body
<[500]> MetaAleph.__init__  # 再次觸發元類中的初始化方法,
<[15]> evaltime_meta module end

# 場景2
$ python3 evaltime_meta.py
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[1]> evaltime_meta module start
<[2]> ClassThree body
<[200]> deco_alpha
<[4]> ClassFour body
<[6]> ClassFive body
<[500]> MetaAleph.__init__
<[9]> ClassSix body
<[500]> MetaAleph.__init__   # 到此行位置,和場景1的情況一樣
<[11]> ClassThree tests ..............................
<[300]> deco_alpha:inner_1   # 方法被類裝飾器替換
<[12]> ClassFour tests ..............................
<[5]> ClassFour.method_y     # 類裝飾器對子類不起作用
<[13]> ClassFive tests ..............................
<[7]> ClassFive.__init__     # 初始化ClassFive的例項five
<[600]> MetaAleph.__init__:inner_2  # 方法被替換
<[14]> ClassSix tests ..............................
<[7]> ClassFive.__init__     # 初始化ClassFive的子類ClassSix的例項six
<[600]> MetaAleph.__init__:inner_2  # 子類的方法也被替換了!
<[15]> evaltime_meta module end
複製程式碼

這組例子再一次證明了類物件在匯入時建立!並且元類對它的類物件的初始化也在匯入時進行。其實,匯入時對於元類來說就是它的執行時。

現在來回答之前留下的兩個問題:

  • 元類只要有例項,元類的__init__方法就一定先於例項的__init__方法先執行。比較這兩者的__init__方法有些牽強,畢竟類物件(例如ClassFive)在執行時建立,因此元類的__init__方法必定在匯入時執行;而類例項在執行時才建立,類物件的__init__方法也就只能在執行時才執行。其實就是一個顯而易見的邏輯:什麼時候建立"例項",就什麼時候執行"類"中的__init__方法咯。不過得清楚,這裡的"例項"和"類"究竟指代的是誰。
  • 上一條解釋其實已經回答了"元類為什麼能覆蓋所有子類的方法"。ClassFive是元類MetaAleph的例項,而不是繼承自MetaAlephClassSix雖繼承自ClassFive,但又不是繼承自MetaAleph,它僅僅只是MetaAleph的又一個例項而已。這裡說的覆蓋對元類而言根本就不是覆蓋,元類僅僅只是在為它的例項的屬性賦值而已:你(ClassSix)只是我(MetaAleph)的例項,你繼承自我的另一個例項(ClassFive),又不是繼承自我,所以你跟我談什麼繼承與覆蓋?我只是在給你的屬性賦值而已!

本文對元類的介紹到此結束。這些內容僅僅只是元類的皮毛。其中有很多地方依然沒有弄懂,繼續努力吧!

5.3 補充

其實如果想彌補本文中類裝飾器的缺陷,可以不用定義元類,現在有更方便的方法:定義特殊方法__init_subclass__。它的作用和本文中的元類一樣,但比建立元類簡單直觀得多。在建立子類時,子類都會被這個方法初始化。

6. 總結

本文首先展示了元類的基本用法:直接用type()函式建立類,然後將元類用到了類工廠函式中。之後加入了一個小插曲,類裝飾器;接著深入介紹了元類的概念性知識,並展示瞭如何使用classmetaclass關鍵字從元類建立類。最後,介紹了執行時與匯入時的概念,並通過程式碼展示了這兩者的區別,尤其展示了類的建立時間。


迎大家關注我的微信公眾號"程式碼港" & 個人網站 www.vpointer.net ~

Python學習之路41-元類

相關文章