淺談 Python Metaclass

Dboy Liao
10 min readOct 6, 2019

--

好久沒寫東西了,來寫個冷門的東西 XD

記得之前聽說一個 talk (忘了是哪一場….Orz),講者是這麼說的

99% 的 Python programmer 都不需要知道什麼是 metaclass

當時還年輕嘛,就想說: 馬的,我要當那個 1% (茶)

但老實說,工作幾年下來,除非你在從事 framework 等級專案的開發,不然一般來說 99% 的問題確實都不需要動用 metaclass 就能處理。

而且就因為 metaclass 給予了很大的自由度讓你控制物件的行為 (譬如你可以說只要有 method 的名字有 void ,你就讓它消失這種事也做得到),一個不小心就會弄炸東西,一般除非萬不得已,也不會輕易動用 metaclass。

但用 metaclass 就是爽 (大誤)

所以說,管他的! 就來聊聊 metaclass 吧。

網路上其實有很多很不錯的資源,但大多都著重在怎麼使用 metaclass 上:

為什麼會有 metaclass 這件事好像沒看到有多少人講,所以我想在這篇閒聊裡說說我對 metaclass 的體會,希望回頭看 python 的 data model 時可以不會覺得在說火星語,也能更善加利用 python 動態的特性。

就從“物件”開始聊吧。

OOP in Python: Everything is an “object”

Python 是個 multi paradigm 的語言,但無可否認,OOP 是它最重要的一部分。

我自己是學數學出身的,對於公理系統總是有種莫名的執著。對於 python 的型別系統,經過幾年經驗下來,我喜歡從兩個公理出發:

1. Everything is an object in Python

2. Every object has a type

公理之所以叫公理就是因為我不知道怎麼證明,但是又想不出為什麼會錯,你可以想成一個學數學的人對你使出了相信我之術,接受就對了。(如果你不能接受,我蠻推薦你去看一本科普書叫 《數學:確定性的失落》,看完你就會相信我了 XD)

接下來我會從這兩條公理,試著給讀者一個思路,可以去反推出說為什麼 metaclass 會存在,自然也就比較容易理解它應該怎麼被使用。

那到底什麼是個 object 呢? 如果我跟你說你自己去看 CPython 原始碼你就會發現有個 struct 叫 PyObject ,那個就是了,我想你應該會覺得我有講等於沒講。

對我來說,關於這個問題我喜歡引用在 C 語言規格書像是 c99 裡面對於 object 的定義:

region of data storage in the execution environment, the contents of which can represent values

白話文來說,就是在程式執行時某一塊記憶體,而其內容可以用來表示值。那再更白話文的說,有個我們每天幾乎都會用的名詞是個不錯的出發點: 東西。沒錯,就是在程式執行的時候,你可以拿來用的東西。你可以試試看給個給東西一個白話文解釋看看….

根據公理 2 ,一但有了東西存在,就存在著型別 (type)。譬如說我們可以說這個東西是一個杯子、那個東西是一個椅子或這個東西是一隻狗等等,杯子、椅子和狗就是這些東西的型別。

抽象化的工具: class

隨便翻一本在講 OOP 的書,前面的章節應該都會提到 OOP 裡面的一些重要概念,譬如說物件的封裝、多型等等。你可能會看到書上例子提到像是 Bark 是一隻狗,有棕色的毛、短尾巴、叫聲是汪汪;另外有隻狗叫 Bob,有黑色的毛、長尾巴、叫聲是旺旺。因為觀察到如果是狗的話,其共通的特徵就是有毛、有尾巴、都會叫,不同的只是一些個體差異譬如毛色、尾巴長度和叫出來的聲音。基於這個觀察,你得到一個 Dog 這個 class 的定義像是這樣:

class Dog(object):
def __init__(self, name, color, tail_length, sound):
self.name = name
self.color = color
self.tail_length = tail_length
self.sound = sound

def bark(self):
print('%s: %s' % (self.name, self.sound))
bark = Dog('Bark', 'brown', 5, '汪汪')
bob = Dog('Bob', 'black', 15, '旺旺')

上面的 code 並不重要,重要的是我們得到這樣 code 前的思路: 我們觀察到共通的特徵,並基於這些特徵定義了一個型別 (class);反過來說,當我們觀察到共通的特徵時,我們可以定義出一個型別。這裡,我們給個公理 3:

3. Whenever there are something in common among objects, we can derive a type/class out of them

Prove me wrong if you don’t agree with me 😜

Metaclass: 型別的型別

我們當然不會只定義一個狗的型別吧?我們也會定義貓、桌子、椅子等等各式各樣的 class ,那狗的 class 可以生出型別是狗的 object 、貓的 class 可以生出貓的 object、桌子椅子的 class 也可以生出各自的 object。這時我們觀察到一個很有趣的共通特徵: 這些型別都可以生出 object 。而且當你寫下 class Dog: ...class Cat: ... 之後,你可以在 python 裡用 DogCat 去生成物件,那 DogCat 本身又是什麼?

還記得公理 1 嗎? 沒錯 ,他們也是 object

根據公理 3 跟 2,這些 class 本身應該也有一個型別才對,那自然得出我們本文的主角: metaclass。

在 python 裡內建的 metaclass 就是 type ,到這裡不難想像 type 的功能就是控制著 class 的生成,就如同 Dog 這個 class 控制著它的實例生成是一樣的道理。那根據公理 1, type 本身也是物件:

但更好玩的來了,在 python 裡, type 是繼承自 object :

也就是說, typeobject 那裡繼承了所有 data model 相關的 method ,諸如 __new____init__ 等,只是 type 生成的實例是 class 。我自己是覺得這是 python 為了 unify 整個 object system 所做出的選擇,並不是非得這樣不可。

那大家可以思考一下: type 可以用來生出 class 物件,且 type 本身也是物件,所有物件都有型別,那 type 的型別是什麼?

回想我們當初怎麼想出 metaclass type 的定義的: 所有可以創造實例的物件的型別。那 type 本身也符合這個定義,因為 type 可以創造出 class 的實例,也就是說 type 本身的型別就是…. type !

口說無憑,眼見為證:

這邊一個初學者常會犯的錯誤就是,以為把 type 當成 function 使用在一個物件上,就會 “print” 出該物件的型別。但實際上, type() 會把該物件的型別本身這個物件回傳給你。所以說這邊確實可以確定 type 本身的型別就是自己。

我想看到這裡,下面這幾條 code 應該都屬於意料之中的事了吧:

自定義 Metaclass

根據 python 的 data modeltype.__new__ 的 signature 是長這樣的:

type.__new__(mcls, name, base, attribs)

  • mcls: metaclass 物件本身
  • name: 要被創建的 class 的名字
  • base: 要被創建的 class 所繼承的其他 class
  • attribs: 要被創建的 class 本身的各項 attribute

舉個例子來說,在 python 裡以下兩種寫法是等價的:

class MyClass(object):
ANSWER = 42

def speak(self):
print('the answer to life is {}'.format(self.ANSWER))
# or
MyClass = type(
'MyClass',
(object,),
{
'ANSWER': 42,
'speak': lambda self: print('the answer to life is {}'.format(self.ANSWER))
}
)

眼尖的讀者或許會問: 那 mcls 跑哪兒去了?

其實當你寫下 type(...) 時,其實 mcls 會是 type 自己。也就是說等同於使用 type.__new__(type, 'MyClass', (object,), ...)

而在 python 裡實作自己的 metaclass ,唯一的方法就是繼承 type

那我們就無聊惡作劇一下,寫個 metaclass 讓所有定義在 class 裡的 method 都消失好了,而且名字會被加上一個 Funky 前綴。

class MyMeta(type):
def __new__(mcls, name, base, attribs):
name = 'Funky'+name
return super().__new__(mcls, name, base, {})

接下來我們用 MyMeta 來建立 MyClass :

class MyClass(object, metaclass=MyMeta):
def awesome_func(self):
return "Awesome!"

神奇的事發生了:

Error message 顯示的 class 名稱變成叫 FunkyMyClass ,而且本來定義好的 awesome_func 就消失了。

這樣不難發現 metaclass 的威力吧。

結語

上面的簡單例子展示了 metaclass 如何去控制 class 的生成,但有個小細節是,如果你自己寫個 metaclass 然後在 __new__ 裡把 python 傳給你的 attribs print 出來看的話,就會發現我上面的例子其實把一些應該要傳進去的雙底線 attribute 都去掉了。

在我們上面的例子裡,可能還沒什麼問題,但在其他狀況,可能就會造成一些預期外的錯誤。這也是為什麼使用 metaclass 時必須特別小心的地方。我個人建議是把 python 的 data model 相關文件擺在旁邊參考是最好的。

另外 metaclass 的使用時機大多是在實現一個 framework,而你希望透過 metaclass 去控制使用者自定義的 class 是否有符合某些條件,譬如說正確 overwrite 某些 method 等等時可以用;另外就是實現一些特殊的語意,譬如說 python 的 enum 模組裡面的許多 class 都是用 metaclass 實現的。

那這次就閒聊到這裡啦。

Happy python programming!

Reference

--

--

Dboy Liao

Code Writer, Math Enthusiast and Data Scientist, yet.