1. 程式人生 > >Python 元類 (MetaClass) 小教程

Python 元類 (MetaClass) 小教程

可能是 Ruby 帶的頭,大家喜歡把“超程式設計”稱作魔法,其實哪有什麼魔法,一切都是科學。而 meta classes 就是 Python 裡最魔法的科學,也是 99% 的人用不到的科學。只是誰還不想學點魔法呢?

(本文使用的語法僅在 Python 3 下有效)

#爺爺 = 元爸爸

Meta is a prefix used in English to indicate a concept which is an abstraction behind another concept, used to complete or add to the latter.

根據維基百科,英語字首 meta-

指對一種抽象概念的抽象,得到另一種概念。比如說程式設計 programming 一般指編寫程式碼來讀取、生成或轉換資料。那麼超程式設計 meta-programming 一般就指人編寫程式碼來讀取、生成或轉換程式碼

聽著很玄幻,但 meta 讓我想到了一首兒歌,有句歌詞:“爸爸的爸爸叫什麼?爸爸的爸爸叫爺爺”,現在有了 meta-,我們可以把爺爺叫作 meta-爸爸(元爸爸)了。

我們知道 Python 裡一切都是物件,那麼是物件就有對應的“類(Class)”,或稱“型別(type)”。 Python 中可以用 type(obj) 來得到物件的“類”:

type(10)#> inttype([1
,2,3])
#> listtype({'a': 1, 'b': 2})#> dictclass DoNothing: passx = DoNothing()type(x)#> __main__.DoNothing

既然一切都是物件,一個“類(class)”也可以認為是一個物件,那麼類的“型別(type)”是什麼呢?

type(int), type(list), type(dict)#> (type, type, type)type(DoNothing)#> type

可以看到,“類(class)”的型別(type) 都是 type。那 type 的型別又是什麼呢?

type(type)#> type

抱歉,type 的型別還是 type,是一個遞迴的型別。

物件的型別叫作類(class),類的型別就稱作元類 meta-class。是不是很像“爸爸的爸爸叫爺爺”?換句話說,“普通類(class)”可以用來生成例項(instance),同樣的,元類 (meta-class)也可以生成例項,生成的例項就是“普通類”了。

#類是動態建立的

我們知道,類(class)可以有多個例項(instance)。而建立例項的方法就是呼叫類的建構函式(constructor):

class Spam(object):    def __init__(self, name):        self.name = namespam = Spam('name')

上例我們定義了一個類,並呼叫類的建構函式建立了該類的一個例項。我們知道類也可以看作類 type 的一個例項,那麼如何用 type 的建構函式來動態建立一個類呢?我們先看看 type 的建構函式

type(name, bases, dict):

  • name: 字串型別,存放新類的名字
  • bases: 元組(tuple)型別,指定類的基類/父類
  • dict: 字典型別,存放該類的所有屬性(attributes)和方法(method)

例如下面的類:

class Base:    counter = 10class Derived(Base):    def get_counter(self):        return self.counterx = Derived()x.get_counter()#> 10

我們可以呼叫 type(...) 來動態建立這兩個類:

Base = type('Base', (), {'counter': 10})Derived = type('Derived', (Base,), dict(get_counter=lambda self: self.counter))x = Derived()x.get_counter()#> 10

是的,你沒有猜錯,Python 在遇到 class ... 關鍵字時會一步步解析類的內容,最終呼叫 type(...) (準確說是指定的元類)的建構函式來建立類,換句話說上面兩種定義類的方式是等價的。在下節我們會具體講解。

#類的建立過程

要了解元類(meta-class)的作用,我們就需要了解 Python 裡類的建立過程 ,如下:

  1. 當 Python 見到 class 關鍵字時,會首先解析 class ... 中的內容。例如解析基類資訊,最重要的是找到對應的元類資訊(預設是 type)。
  2. 元類找到後,Python 需要準備 namespace (也可以認為是上節中 typedict 引數)。如果元類實現了 __prepare__ 函式,則會呼叫它來得到預設的 namespace 。
  3. 之後是呼叫 exec 來執行類的 body,包括屬性和方法的定義,最後這些定義會被儲存進 namespace。
  4. 上述步驟結束後,就得到了建立類需要的所有資訊,這時 Python 會呼叫元類的建構函式來真正建立類。

如果你想在類的建立過程中做一些定製(customization)的話,建立過程中任何用到了元類的地方,我們都能通過覆蓋元類的預設方法來實現定製。這也是元類“無所不能”的所在,它深深地嵌入了類的建立過程。

#元類的應用

元類就是深度的魔法,99%的使用者應該根本不必為此操心。如果你想搞清楚究竟是否需要用到元類,那麼你就不需要它。那些實際用到元類的人都非常清楚地知道他們需要做什麼,而且根本不需要解釋為什麼要用元類。

Python界的領袖 Tim Peters

為了文章的完整性,以及日後查閱方便,這裡還是要舉兩個例子的。順帶一提,下面這兩個例子在 Python 3.6 之後都可以通過覆蓋基類的 __init_subclass__ 來實現,而不需要通過元類實現。

#強制子類實現特定方法

假設你是一個庫的作者,例如下面的程式碼,其中的方法 foo 要求子類實現方法 bar

# library codeclass Base(object):    def foo(self):        return self.bar()# user codeclass Derived(Base):    def bar():        return None

但作為庫的作者,我們根本無法預測使用者會寫出什麼樣的程式碼,有什麼方法能強制使用者在子類中實現方法 bar 呢?用 meta-class 可以做到。

class Meta(type):    def __new__(cls, name, bases, namespace, **kwargs):        if name != 'Base' and 'bar' not in namespace:            raise TypeError('bad user class')        return super().__new__(cls, name, bases, namespace, **kwargs)class Base(object, metaclass=Meta):    def foo(self):        return self.bar()

現在,我們嘗試定義一個不包含 bar 方法的子類,在類的定義(或者說生成)階段就會報錯:

>>> class Derived(Base):...     pass...Traceback (most recent call last):  File "<stdin>", line 1, in <module>  File "<stdin>", line 4, in __new__TypeError: bad user class

#註冊所有子類

有時我們會希望獲取繼承了某個類的子類,例如,實現了基類 Fruit,想知道都有哪些子類繼承了它,用元類就能實現這個功能:

class Meta(type):    def __init__(cls, name, bases, namespace, **kwargs):        super().__init__(name, bases, namespace, **kwargs)        if not hasattr(cls, 'registory'):            # this is the base class            cls.registory = {}        else:            # this is the subclass            cls.registory[name.lower()] = clsclass Fruit(object, metaclass=Meta):    passclass Apple(Fruit):    passclass Orange(Fruit):    pass

之後,我們可以檢視所有 Fruit 的子類:

>>> Fruit.registory{'apple': <class '__main__.Apple'>, 'orange': <class '__main__.Orange'>}

#new vs init

上面的例子中我們分別用了 __new____init__,但其實這兩個例子裡用哪種方法都是可行的。

__new__ 用來建立一個(未初始化)例項;__init__ 則是用來初始化一個例項。在元類的 __new__ 方法中,因為類例項還沒有建立,所以可以更改最後生成類的各項屬性:諸如名稱,基類或屬性,方法等。而在 __init__ 中由於類已經建立完成,所以無法改變。正常情況下不需要關心它們的區別。

#小結

  • 物件的型別稱為類,類的類就稱為元類。
  • Python 中對元類例項化的結果就是“普通類”,這個過程是動態的。
  • 在定義類時可以指定元類來改變類的建立過程。

請你相信,作為平民百姓,咱們是沒有機會用到魔法的。但學習本身對於瞭解語言的設計是很有幫助的,何況萬一有個萬一呢◔_◔?

#參考