先以一個大牛的一段關於Python Metapgramming的著名的話來作開頭:python
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 Petersexpress
這是什麼鬼話?道可道,很是道嗎?編程
好,裝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這個詞天朝這邊翻譯成「元」,海峽對岸翻譯成「後設」。其實我都不大理解從何而來。code
聚焦到咱們今天的主題,metaprogramming就是編寫用來生成代碼的代碼。ip
假設咱們寫了一個NB的函數,用來計算一個任意複雜的算數表達式的值:
像1+2, 3*6+10, 什麼的均可以交給它去計算。這樣的函數的算法不是咱們的主題,因此咱們請出python自帶的大招eval()
,一行就能夠搞定了:
def calc(expression): return eval(expression)
由於輸入的可能性是無限的,因此咱們確定要好好測試一下這個函數了。假定咱們想了 上百個test case。又假定咱們是用unittest
這個module來作測試的。這樣的測試程序通常會長成這樣:
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的方式來自動產生相似上面的測試類。
先上程序後解釋:
#!/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)
確實是一個類的方法,只不過這個類是咱們經過代碼動態生成的。也就是下面這一行:
cls = type(name, (unittest.TestCase,), d)
這裏的type()
就是一般咱們用來檢查某個變量的類型的那個函數。只不過它還有另一種不大爲人知的形式:
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會作到事半功倍。祝你在實踐中碰到這樣的機會。