這是Python直譯器簡介的第四部分。閱讀第一部分、第二部分和第三部分。如果你喜歡這個系列的話,那就把它分享到Hacker School吧。我是那裡的管理員。
當我開始研究python內部工作的時候,我一直很困惑為什麼能夠進行“編譯”的python還是“動態語言”。通常,我們都將這兩個詞作為一對反義詞——“動態語言”1包括Python、Ruby和 Javascript等,而“編譯語言”則包括C、Java和Haskell等。
人們通常所說的“編譯語言”是指能夠編譯出適用於x86、ARM等的指令2(作用於真正的機器)的語言。一種“解釋性”語言不是根本就沒有編譯器3就是隻編譯成一箇中間表示,比如位元組碼。位元組碼的指令不是作用於任何硬體的,而是虛擬機器。Python就屬於後者:Python的編譯器將生成的位元組碼傳遞給Python直譯器。4
Python直譯器將通過虛擬機器做許多工作使得位元組碼得以解釋。至於虛擬機器,我們會在第五部分討論。
目前為止,我們對編譯和解釋的概念還是抽象的。通過下面這個例子我們能更加清晰:
1 2 3 4 5 6 7 8 9 10 |
>>> def modulus(x, y): ... return x % y ... >>> [ord(b) for b in modulus.func_code.co_code] [124, 0, 0, 124, 1, 0, 22, 83] >>> dis.dis(modulus.func_code) 2 0 LOAD_FAST 0 (x) 3 LOAD_FAST 1 (y) 6 BINARY_MODULO 7 RETURN_VALUE |
這是一個函式以及它的位元組碼通過反彙編程式的結果。一旦我們定義了modulus函式,它就被編譯了並且生成了一個不能被修改的程式碼物件。
這應該很容易推算。鍵入modulus(%)使編譯器發出指令BINARY_MODULO。所以如果我們要計算一個餘數的話,這個函式就能夠發揮作用了。
1 2 |
>>> modulus(15,4) 3 |
這樣看,它正常工作。但如果我們不傳遞數字給它呢?
1 2 |
>>> modulus("hello %s", "world") 'hello world' |
慢著,這是怎麼了?你以前也許見到過,但它通常是這麼寫的:
1 2 |
>>> print "hello %s" % "world" hello world |
當BINARY_MODULO處理兩個字串的時候,它預設執行字串插值而不是求餘數。這就是動態型別的典型例子。編譯器在生成modulus的程式碼物件的時候,它完全不知道x和 y是字串、數字還是其它型別。它只是發出一些指令而已:載入一個名字,載入另一個名字,BINARY_MODULO這個兩個物件,然後返回結果。至於弄清BINARY_MODULO真正指什麼則是直譯器的工作。
我想我們忽略了一些東西。我們的函式modulus能夠計算餘數或者格式化字串,還有嗎?如果我們定義一個能夠響應__mod__的自定義物件的話,我們還能做很多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
>>> class Surprise(object): ... def __init__(self, num): ... self.num = num ... def __mod__(self, other): ... return self.num + other.num ... >>> seven = Surprise(7) >>> four = Surprise(4) >>> modulus(seven, four) 11 >>> modulus(7,4) 3 >>> modulus("hello %s", "world") 'hello world' |
雖然還是那個有著同樣位元組碼的函式modulus,但是當它傳遞不同種物件的時候,作用並不相同。Modulus也可能報錯——比如,如果我們使用一個不能執行__mod__的物件就會有TypeError。更不可思議的是,當__mod__被呼叫的時候,我們能寫出一個造成SystemExit的自定義物件。__mod__函式能夠對檔案進行寫入操作;改變一個全域性變數或者刪除物件的另外一個性質。我們幾乎能做任何事。
這也是難以優化Python的原因之一:在編譯程式碼物件和生成位元組碼的時候,你並不知道會有怎樣的結果。編譯器根本不關心結果如何。就像Russell Power 和 Alex Rubinsteyn 所說的:“我們能使解釋性語言Python有多快?”,“由於不用宣告型別資訊,幾乎每個指令都要像INVOKE_ARBITRARY_METHOD一樣來執行。”
儘管“編譯”和“解釋”的定義在通常情況下是很難區分的,但是對於Python來說卻很簡單。編譯工作就是生成程式碼物件,包括位元組碼。而翻譯的工作就是翻譯位元組碼,執行指令。Python保持“動態”特性的原因之一就是同樣的位元組碼能夠有不同的作用。更普遍的說法就是Python直譯器的工作比編譯器的多一些。
在第五部分,我們就會討論虛擬機器和直譯器了。
注:
- 你通常聽到的“解釋語言”就是“動態語言”。
- 感謝David Nolen給出了這個定義。“語法分析”、“編譯”和“解釋”的定義很易混淆。
- 一些語言並不在R、Scheme、二進位制檔案中都進行編譯。這與你使用的開發工具和你對“編譯”的定義有關。
- 儘管我和以往一樣是基於CPython 和Python 2.7來寫本文的,但是本文大部分內容也適於其它開發工具。