接觸過 Django 的同學都應該十分熟悉它的 ORM 系統。對於 python 新手而言,這是一項幾乎可以被稱作“黑科技”的特性:只要你在models.py
中隨便定義一個Model
的子類,Django 便可以:
- 獲取它的欄位定義,並轉換成表結構
- 讀取
Meta
內部類,並轉化成相應的配置資訊。對於特殊的Model
(如abstract
、proxy
),還要進行相應的轉換 - 為沒有定義
objects
的Model
加上一個預設的Manager
開發之餘,我也曾腦補過其背後的原理。曾經,我認為是這樣的:
啟動時,遍歷
models.py
中的所有屬性,找到Model
的子類,並對其進行上述的修改。
當初,我還以為自己觸碰到了真理,並曾將其應用到實際生產中——為 SAE 的 KVDB 寫了一個類 ORM 系統。然而在實現的過程中,我明顯感受到了這種方法的醜陋,而且效能並不出色(因為要遍歷所有的定義模組)。
那麼事實上,Django 是怎麼實現的呢?
自古以來我們製造東西的方法都是“自上而下”的,是用切削、分割、組合的方法來製造。然而,生命是自下而上地,自發地建造起來的,這個過程極為低廉。
——王晉康 《水星播種》
這句話揭示了生命的神奇所在:真正的生命都是由基本物質自發構成的,而非造物主流水線式的加工。
那麼,如果 類 也有生命的話,對它自己的修飾就不應該由呼叫者來完成,而應該是自發的。
幸而,python 提供了造物主的介面——這便是 Meta Classes,或者稱為“元類”。
元類 是什麼?
簡單說:元類就是類的類。
首先,要有一個概念:
python 中,一切都是物件。
沒錯,一切,包括 類 本身。
既然,類 是 物件,物件 是 類的例項,那麼——類 也應該有 類 才對。
類的類:type
在 python 中,我們可以用type
檢測一個物件的類,如:
1 |
print type(1) # |
如果對一個類操作呢?
1 2 3 4 5 6 7 |
print type(int) # class MyClass(object): pass print type(MyClass) # print type(type) # |
這說明:type
其實是一個型別,所有類——包括type
自己——的類都是type
。
type 簡介
從 官方文件 中,我們可以知道:
- 和
dict
類似,type
也是一個工廠建構函式,呼叫其將返回 一個type
型別的例項(即 類)。 type
有兩個過載版本:type(object)
,即我們最常用的版本。type(name, bases, dict)
,一個更強大的版本。通過指定 類名稱(name
)、父類列表(bases
)和 屬性字典(dict
) 動態合成一個類。
下面兩個語句等價:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Integer(int): name = 'my integer' def increase(self, num): return num + 1 # ------------------- Integer = type('Integer', (int, ), { 'name': 'my integer', 'increase': lambda self, num: num + 1 # 很酷的寫法,不是麼 }) |
也就是說:類的定義過程,其實是type
型別例項化的過程。
然而這和修飾一個已定義的類有什麼關係呢?
當然有啦~既然“類的定義”就是“type
型別的初始化過程”,那其中必定會呼叫到type
的建構函式(__new__()
或 __init__()
)。只要我們繼承 type
類 並修改其 __new__
函式,在這裡面動手腳就可以啦。
接下來我們將通過一個栗子感受 python 的黑魔法,不過在此之前,我們要先了解一個語法糖。
__metaclass__ 屬性
有沒覺得上面第二段示例有些鬼畜呢?它勒令程式設計師將類的成員寫成一個字典,簡直是反人類。如果我們真的是要通過修改 元類 來改變 類 的行為的話,似乎就必須採用這種方法了~~簡直可怕~~
好在,python 2.2 時引進了一個語法糖:__metaclass__
。
1 2 3 |
class Integer(int): __metaclass__ = IntMeta |
現在將會等價於:
1 |
Integer = IntMeta('Integer', (int, ), {}) |
由此一來,我們在使用傳統類定義的同時,也可以使用元類啦。
栗子:子類淨化器
需求描述
你是一個有語言潔癖的開發者,平時容不得別人講一句髒話,在開發時也是如此。現在,你寫出了一個非常棒的框架,並馬上要將它公之於眾了。不過,你的強迫症又犯了:如果你的使用者在程式碼中寫滿了髒話,怎麼辦?豈不是玷汙了自己的純潔?
假如你就是這個喪心病狂的開發者,你會怎麼做?
在知道元類之前,你可能會無從下手。不過,這個問題你可以用 元類 輕鬆解決——只要在類定義時過濾掉不乾淨的字眼就好了(百度貼吧的幹活~~)。
我們的元類看起來會是這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
sensitive_words_list = ['asshole', 'fuck', 'shit'] def detect_sensitive_words(string): '''檢測敏感詞彙''' words_detected = filter(lambda word: word in string.lower(), sensitive_words_list) if words_detected: raise NameError('Sensitive words {0} detected in the string "{1}".' .format( ', '.join(map(lambda s: '"%s"' % s, words_detected)), string ) ) class CleanerMeta(type): def __new__(cls, class_name, bases, attrs): detect_sensitive_words(class_name) # 檢查類名 map(detect_sensitive_words, attrs.iterkeys()) # 檢查屬性名 print "Well done! You are a polite coder!" # 如無異常,輸出祝賀訊息 return super(CleanerMeta, cls).__new__(cls, class_name, bases, attrs) # 重要!這行一定不能漏!!這回撥用內建的類構造器來構造類,否則定義好的類將會變成 None |
現在,只需這樣定義基類:
1 2 3 4 5 |
class APIBase(object): __metaclass__ = CleanerMeta # ... |
那麼所有 APIBase
的派生類都會接受安全審查(奸笑~~):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class ImAGoodBoy(APIBase): a_polite_attribute = 1 # [Output] Well done! You are a polite coder! class FuckMyBoss(APIBase): pass # [Output] NameError: Sensitive words "fuck" detected in the string "FuckMyBoss". class PretendToBePolite(APIBase): def __fuck_your_asshole(self): pass # [Output] NameError: Sensitive words "asshole", "fuck" detected in the string "_PretendToBePolite__fuck_your_asshole". |
看,即使像最後一個例子中的私有屬性也難逃審查,因為它們本質都是相同的。
甚至,你還可以對有問題的屬性進行偷偷的修改,比如 讓不文明的函式在呼叫時打出一行警告 等等,這裡就不多說了。
元類 在實際開發中的應用
日常開發時,元類 常用嗎?
當然,Django 的 ORM 就是一個例子,大名鼎鼎的 SQLAlchemy 也用了這種黑魔法。
此外,在一些小型的庫中,也有 元類 的身影。比如 abc
(奇怪的名字~~)——這是 python 的一個內建庫,用於模擬 抽象基類(Abstract Base Classes)。開發者可以使用 abc.abstractmethod
裝飾器,將 指定了 __metaclass__ = abc.ABCMeta
的類的方法定義成 抽象方法,同時這個類也成了 抽象基類,抽象基類是不可例項化的。這便實現了對 抽象基類 的模擬。
倘若你也有需要動態修改類定義的需求,不妨也試試這種“黑魔法”。
小結
- 類 也是 物件,所有的類都是
type
的例項 - 元類(Meta Classes)是類的類
__metaclass__ = Meta
是Meta(name, bases, dict)
的 語法糖- 可以通過過載元類的
__new__
方法,修改 類定義 的行為