先以一個大牛的一段關於Python Metapgramming的著名的話來做開頭:
Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t (the people who actually need them know with certainty that they need them, and don’t need an explanation about why). – Tim Peters
翻譯一下:Metaclasses是99%的使用者都無需費神的黑科技。如果你還在糾結你是不是需要它的話,答案是NO (真正需要的人根本不需要解釋) – Tim Peters
這是什麼鬼話?道可道,非常道嗎?
Meta?
好,裝B已畢。這確實是一個冷僻的,不常用的話題。一篇短文肯定講不完。 所以叫做初探。
英文meta這個詞其實是從希臘語裡面借來的。wikipedia上的解釋是:
indicate a concept which is an abstraction behind another concept, used to complete or add to the latter
不看還好,其實看了更暈。好在後面的解釋有一句“更高一層的抽象”,可以幫助理解。 其實我們可以這樣理解。meta的意思就是“關於什麼的什麼”:比如metadata可以理解為“關於資料的資料”,metaprogramming可以理解為“關於程式設計的程式設計”。這就和“更高一層的抽象” 比較契合了。同時又隱隱和程式設計中的另一個永恆主題-recursion聯絡在了一起。
另外,meta這個詞天朝這邊翻譯成“元”,海峽對岸翻譯成“後設”。其實我都不大理解從何而來。
例項
聚焦到我們今天的主題,metaprogramming就是編寫用來生成程式碼的程式碼。
假設我們寫了一個NB的函式,用來計算一個任意複雜的算數表示式的值:
像1+2, 3*6+10, 什麼的都可以交給它去計算。這樣的函式的演算法不是我們的主題,所以我們請出python自帶的大招eval(),一行就可以搞定了:
1 2 |
def calc(expression): return eval(expression) |
因為輸入的可能性是無限的,所以我們肯定要好好測試一下這個函式了。假定我們想了 上百個test case。又假定我們是用unittest這個module來做測試的。這樣的測試程式一般會長成這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import unittest class TestStringMethods(unittest.TestCase): def test_upper(self): self.assertEqual('foo'.upper(), 'FOO') def test_isupper(self): self.assertTrue('FOO'.isupper()) self.assertFalse('Foo'.isupper()) def test_split(self): s = 'hello world' self.assertEqual(s.split(), ['hello', 'world']) # check that s.split fails when the separator is not a string with self.assertRaises(TypeError): s.split(2) if __name__ == '__main__': unittest.main() |
所以我們的目的就是用metaprogramming的方式來自動產生類似上面的測試類。
先上程式後解釋:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#!/usr/bin/python3 import unittest def calc(expression): return eval(expression) def add_test(name, asserts): def test_method(asserts): def fn(self): left, right = asserts.split('=') expected = str(calc(left)) self.assertEqual(expected, right) return fn d = {'test1': test_method(asserts)} cls = type(name, (unittest.TestCase,), d) globals()[name] = cls if __name__ == '__main__': for i, t in enumerate([ "1+2=3", "3*5*6=90"]): add_test("Test%d" % i, t) unittest.main() |
NB的calc()函式我們解釋過了。main這段也比較簡單:我們用宣告的方式定義了一組測試,然後通過unittest來執行。
有點複雜的是add_test()。我們先來看看最內層的fn(self)這個方法。邏輯上,它就是把輸入的測試用例分成兩份,一份是calc()的輸入,一份是我們期待的結果;然後呼叫calc(), 接著用assertEqual()來測試。
但是這個self有點奇怪 – 這裡沒有類,哪裡來的self? 其實fn(self)確實是一個類的方法,只不過這個類是我們通過程式碼動態生成的。也就是下面這一行:
1 |
cls = type(name, (unittest.TestCase,), d) |
這裡的type()就是通常我們用來檢查某個變數的型別的那個函式。只不過它還有另外一種不大為人知的形式:
1 |
class type(name, bases, dict) |
這第二種形式,就會產生一個新的型別。以我們的程式為例,就是以unit.TestCase為baseclass, 產生了一個名為TestN的新型別,改型別的實現由d給出,而d就包含了通過closure返回的fn(self)這個方法。只不過在這個新類裡面,它的名字叫做 test1()。
最後,我們把這個新產生的類加入到當前全域性符號表裡面,也就相當於上面給出的unittest的例子。
所以,總結一下。當我們執行這個指令碼的時候,這段比較短的程式碼會針對每一個測試的表示式產生一個新的測試類,並動態生成測試的方法載入到該類裡面。unitest從globals中找到這些類並一一執行測試。
上面的例子中,其實一行一行手打calc(1+2) == 3也沒什麼大不了的。但是當你要表達的邏輯比較複雜的時候,metaprogramming的強大就體現出來了。
總結
那麼,看完這篇文章,我們也成為Tim所說的1%的程式猿了!其實,也許他的意思是,99%的程式設計工作都用不到這樣技巧。在一些特殊的場合,比如編寫某種框架的時候,metaprogramming會做到事半功倍。祝你在實踐中碰到這樣的機會。