Python 元類

Yabea發表於2021-01-18

印象中,是在建立單例模式時知道可以用到元類(metaclass),但始終對其瞭解的不是很透徹,很多人也都說元類是Python中較難理解的概念之一,於是找來幾本書,希望可以找到答案,本文以Python3為例。

本文參考:

《人人都懂設計模式》

《Python Cookbook》

《 流暢的Python》

先來簡單介紹下:元類(metaclass)是一個類,你也可以理解為類的類,因為Python中的類是在執行時動態建立的,那麼通過元類便可以控制類屬性和類例項的建立過程。

來看看用元類實現的單例模式:

class Singleton(type):
    """
    單例模式
    """
    def __init__(cls, *args, **kwargs):
        cls.__instance = None
        super().__init__(*args, **kwargs)

    def __call__(cls, *args, **kwargs):
        if cls.__instance is None:
            cls.__instance = super().__call__(*args, **kwargs)
        return cls.__instance


class Test(metaclass=Singleton):

    def __init__(self):
        pass


a = Test()
b = Test()
print(id(a))
print(id(b))

具體的實現是:建立類時顯式的指定類的metaclass,而自定義的metaclass繼承type,並重新實現__call__方法。

於是,有了兩個問題:

  • 為什麼自定義的metaclass繼承type?

因為,在Python中,type是預設的metaclass(內建元類),Python允許我們自定義metaclass,自定義的metaclass必須繼承自type,也就是:元類從type類繼承了構建類的能力。

我們通常用type來獲取物件所屬的類,就像這樣:

In [10]: a = 10

In [11]: type(a)
Out[11]: int

然而,type還是一個類,你可以通過type來新建一個類,看type的原始碼,通過type(name, bases, dict)便可以生成一個新的類:

In [44]: test_class = type('Test', (), dict({'name': None}))

In [45]: a = test_class()

In [46]: a.name = 'Tony'

In [47]: a.name
Out[47]: 'Tony'

預設情況下,Python中類都是type類的例項:

In [12]: class A:
    ...:     pass
    ...:

In [13]: A.__class__
Out[13]: type

In [14]: int.__class__
Out[14]: type

當你使用class關鍵字時,Python在幕後做的事情,就是通過元類來實現的。

  • 為什麼重新定義__call__方法?

提出該問題是因為,與Python類建立相關的方法是:

__new__:類方法,負責物件的建立,在定義類時需要返回一個例項,在我們通過類名進行例項化物件時自動呼叫。
__init__:初始化函式,負責對new例項化的物件進行初始化,負責物件狀態的更新和屬性的設定,在每一次例項化物件之後呼叫。

而我們常用__call__方法只是為了宣告這個類的物件是可呼叫的(callable)。

但是,在metaclass中__call__方法還負責物件的建立,這就是為什麼要重新定義的原因了。

重定義了__call__方法之後,一個物件的建立過程大概如下圖:

我們驗證一下:

class TestMetaClass(type):

    def __init__(cls, what, bases=None, dict=None):
        print("metaclass init")
        super().__init__(what, bases, dict)

    def __call__(cls, *args, **kwargs):
        print("metaclass call")
        self = super(TestMetaClass, cls).__call__(*args, **kwargs)
        return self


class TestClass(metaclass=TestMetaClass):

    def __init__(self, *args, **kwargs):
        print("class init")
        super().__init__()

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

a = TestClass()

返回:

metaclass init
metaclass call
class new
class init

可以看到,__call__方法在類執行__new____init__之前執行,這樣就可以解釋:

在Singleton中的__call__方法對類屬性__instance進行判斷:

  1. 如果__instance為None,表明類還未進行例項化,那麼給__instance賦值為元類的父類(type)的__call__方法。
  2. 如果__instance不為None,說明類已經進行過例項化,直接返回cls.__instance中的類例項。

便實現了單例模式。

除了重新定義__call__以外,元類可以通過實現__init__方法來定製例項,元類的__init__方法可以做到類裝飾器能做到的任務事情,並且作用更大。

如果想要進一步定製類,可以在元類中實現__new__方法。

另,編寫元類時,通常會把self引數改為cls,這樣能更清楚的表明要構建的例項是類。

元類的呼叫

上述例子中,都是通過metaclass=''來設定類的元類,還可以這樣:

class TestClass():
    __metaclass__ = TestMetaClass

    def __init__(self, *args, **kwargs):
        print("class init")
        super().__init__()

在執行類定義時,直譯器會先尋找這個類屬性中的__metaclass__,如果此屬性存在,就將這個屬性賦值給此類作為它的元類,如果此屬性沒有定義的話,就會向上查詢父類的__metaclass__,如果沒有發現任何的父類,並且直譯器中也沒有名字為__metaclass__的全域性變數,這個類就是傳統類,會使用type.ClassType作為此類的元類。

以上。

相關文章