這是Python直譯器簡介的第三部分。第一部分請點選這裡。第二部分請點選這裡。如果你喜歡這個系列的話,那就把它分享到Hacker School吧。我是那裡的管理員。
位元組碼
在上一章結尾,我們看到了一些奇怪的字元輸出:
1 2 |
>>> foo.func_code.co_code 'd\x01\x00}\x01\x00|\x01\x00|\x00\x00\x17S' |
這就是python的位元組碼。
看完第二部分的講解後,你應該知道“python位元組碼”和 “python程式碼物件”是不相同的。位元組碼是程式碼物件眾多屬性中的一個。我們在程式碼物件的co_code 屬性下找到了位元組碼。它包含了各種作用於直譯器的指令。
那麼什麼是位元組碼呢?其實,它就是一系列的位元組。這些位元組列印出來的樣子很奇怪是因為有些位元組是能夠列印的而有些不能。從分析ord的每個位元組中我們看到它們只是數字而已。
1 2 |
>>> [ord(b) for b in foo.func_code.co_code] [100, 1, 0, 125, 1, 0, 124, 1, 0, 124, 0, 0, 23, 83] |
這就是那些組成python位元組碼的位元組。直譯器會迴圈接收各個位元組,查詢每個位元組的指令然後執行這個指令。需要注意的是,位元組碼本身並不包括任何python物件,或引用任何物件。
如果你想知道python位元組碼的意思,可以去找到CPython直譯器檔案(ceval.c),然後查閱100的意思、1的意思、0的意思,等等。在後續內容中,我們會這麼做的!但暫時可以用更簡單的方法: dis 模組。
反彙編位元組碼
反彙編位元組碼的意思就是接收這一系列的位元組,然後列印出我們能夠理解的字元。這並不是python的工作; dis 模組只是幫助我們瞭解python內部工作的中間狀態。我不支援在產品程式碼中使用 dis ——它是面向程式設計師的,不是電腦。
但是,現在我們需要做的正是讓一些位元組碼變得更通俗易懂,所以 dis 是一個非常理想的工具。我們將使用 dis.dis 函式去分析 foo 函式的程式碼物件。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>>> def foo(a): ... x = 3 ... return x + a ... >>> import dis >>> dis.dis(foo.func_code) 2 0 LOAD_CONST 1 (3) 3 STORE_FAST 1 (x) 3 6 LOAD_FAST 1 (x) 9 LOAD_FAST 0 (a) 12 BINARY_ADD 13 RETURN_VALUE |
(你通常會看到這種寫法:dis.dis(foo),直接分析它的函式物件。這其實是一種簡便寫法: dis 真正分析的還是程式碼物件。如果要傳遞一個函式,那麼只能接收到它的程式碼物件。)
左邊那一列數字是原始原始碼的行號。第二列是位元組碼的偏移量:LOAD_CONST在第0行,STORE_FAST在第3行,以此類推。中間那列是位元組的名字。它們是為程式設計師所準備的——直譯器是完全不需要的。
最後兩列告訴我們一些關於指令引數(如果有的話)的細節。第四列是引數本身。它表示一個指向程式碼物件其它屬性的索引。在這個例子中,LOAD_CONST的引數指向列表co_consts,STORE_FAST的引數指向co_varnames。dis在第四列所指向的的地方查詢常數或者名稱, 最後在第五列返回給我們它找到的資料。這很容易就能得到證實了:
1 2 3 4 |
>>> foo.func_code.co_consts[1] 3 >>> foo.func_code.co_varnames[1] 'x' |
這也能解釋為什麼第二個指令STORE_FAST位於位元組碼的第三行。如果一個位元組碼有引數的話,那麼它相鄰的兩個位元組的引數和它相同。當然,正確的處理這些資料是直譯器的工作。
(你也許認為BINARY_ADD應該有引數。這個問題我們會在後面講到直譯器本身的時候再來討論。)
大家通常把dis當作是python位元組碼的反彙編程式。這肯定是正確的(dis模組檔案中就是這麼說的)但同時dis也有其它“法力”:它利用整個程式碼物件列印出程式設計師能理解的輸出。中間三列的資訊實際上是被編碼在位元組碼裡的,而第一列和最後一列則告訴我們其它的資料。總而言之,位元組碼本身其實很簡單:它只是一系列的數字而已;它並不包含任何的名字或常數。
那麼dis模組是怎麼做到將位元組和名字對應,如100和LOAD_CONST,並返回的呢?試想一下你會怎麼做。如果你的想法是:“嗯……你可以建立一個列表並將位元組名按序排列,”或者, “我覺得你可以建立一個字典。然後將名字作為鍵,將位元組值作為值。”那就恭喜你啦!這正是它的做法。檔案opcode.py就定義了你所想的那個列表和字典。它的內容基本都是這樣的(def_op插入了列表和字典中的對映關係):
1 2 3 4 |
def_op('LOAD_CONST', 100) # Index in const list def_op('BUILD_TUPLE', 102) # Number of tuple items def_op('BUILD_LIST', 103) # Number of list items def_op('BUILD_SET', 104) # Number of set items |
看,它甚至有可愛的註釋來解釋每個位元組引數的意思。
好啦,我們現在知道了python位元組碼是什麼(和它不是什麼)還能使用dis來分析它。在第四部分,我們會從另一個例子來了解Python是怎麼做到能生成位元組碼的同時,仍然是動態語言的。