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-
meta-programming
一般就指人編寫程式碼來讀取、生成或轉換程式碼。
聽著很玄幻,但 meta 讓我想到了一首兒歌,有句歌詞:“爸爸的爸爸叫什麼?爸爸的爸爸叫爺爺”,現在有了 meta-
,我們可以把爺爺叫作 meta-爸爸
(元爸爸)了。
我們知道 Python 裡一切都是物件,那麼是物件就有對應的“類(Class)”,或稱“型別(type)”。
Python 中可以用 type(obj)
來得到物件的“類”:
type(10)#> inttype([1 |
既然一切都是物件,一個“類(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 裡類的建立過程 ,如下:
- 當 Python 見到
class
關鍵字時,會首先解析class ...
中的內容。例如解析基類資訊,最重要的是找到對應的元類資訊(預設是type
)。 - 元類找到後,Python 需要準備 namespace (也可以認為是上節中
type
的dict
引數)。如果元類實現了__prepare__
函式,則會呼叫它來得到預設的 namespace 。 - 之後是呼叫
exec
來執行類的 body,包括屬性和方法的定義,最後這些定義會被儲存進 namespace。 - 上述步驟結束後,就得到了建立類需要的所有資訊,這時 Python 會呼叫元類的建構函式來真正建立類。
如果你想在類的建立過程中做一些定製(customization)的話,建立過程中任何用到了元類的地方,我們都能通過覆蓋元類的預設方法來實現定製。這也是元類“無所不能”的所在,它深深地嵌入了類的建立過程。
#元類的應用
元類就是深度的魔法,99%的使用者應該根本不必為此操心。如果你想搞清楚究竟是否需要用到元類,那麼你就不需要它。那些實際用到元類的人都非常清楚地知道他們需要做什麼,而且根本不需要解釋為什麼要用元類。
為了文章的完整性,以及日後查閱方便,這裡還是要舉兩個例子的。順帶一提,下面這兩個例子在 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 中對元類例項化的結果就是“普通類”,這個過程是動態的。
- 在定義類時可以指定元類來改變類的建立過程。
請你相信,作為平民百姓,咱們是沒有機會用到魔法的。但學習本身對於瞭解語言的設計是很有幫助的,何況萬一有個萬一呢◔_◔?