Python 編譯:code物件 與 pyc檔案

發表於2016-01-09

執行程式

當在shell中敲入python xx.py執行 Python 程式時,就是啟用了 Python 直譯器。

Python 直譯器並不會立即執行程式,而是會對 Python 程式的原始碼進行編譯,產生位元組碼,然後將位元組碼交給虛擬機器一條條順序執行。

原始檔中的內容可以分為:字串常量操作

操作會被編譯為位元組碼指令序列字串常量在編譯的過程中會被收集起來。這些編譯後的資訊在程式執行時,會作為 執行時物件 PyCodeObject 儲存於記憶體中。執行結束後,PyCodeObject 被放入xx.pyc檔案,儲存在硬碟中。這樣,在下次執行時,可以直接根據.pyc檔案的內容,在記憶體中建立 PyCodeObject ,不需要再進行編譯。

PyCodeObject

在編譯器對原始碼進行編譯時,會為每一個 Code Block 建立一個對應的 PyCodeObject。那麼,什麼是 Code Block 呢?規則是:當進入一個新的名字空間,或者新的作用域,就是進入了一個新 Code Block。名字空間是符號的上下文環境,決定了符號的含義。也就是說,決定了變數名對應的變數值是什麼。

名字空間是可以巢狀的,能夠形成一個名字空間鏈,虛擬機器在執行位元組碼時,一個重要的任務就是從鏈中確定一個符號的物件是什麼。

在 Python 中,類、函式、modules 對應獨立的名字空間,所以都有對應的 PyCodeObject。

PyCodeObject 中co_code域儲存的就是對操作編譯生成的位元組碼指令序列

產生pyc檔案的方法

上面提到,Python 程式執行結束後,會在硬碟中以.pyc檔案的形式儲存 PyCodeObject,但直接執行 Python 程式並不會產生.pyc檔案。

這可能是因為直接執行的 Python 程式,有些只是臨時使用一次,所以沒有通過.pyc儲存編譯結果的必要。

一種常見的,產生pyc檔案的方法是import機制。當Python 程式執行時,如果遇到 import abc,會到設定好的path中尋找 abc.pyc 檔案,如果沒有,只找到abc.py,會先將 abc.py 編譯成 CodeObject,然後建立 pyc 檔案,將 CodeObject寫入,最後才會對 pyc 進行import操作,將 pyc 中的 CodeObject重新複製到記憶體,並執行。

另外,Python 標準庫中的py_compilecompile可以幫助手動產生 pyc 檔案。

pyc 檔案內容是二進位制的,想要了解 pyc 檔案的格式,就要了解 PyCodeObject 中各個域的作用。

PyCodeObject域

在 Python 中訪問 PyCodeObject

C語言形式的 PyCodeObject 對應 Python 中的 Code物件,Code物件 是對 PyCodeObject 的簡單包裝。

因此,可以通過 Code物件 訪問 PyCodeObject 的各個域。這就需要使用 內建函式 compile。

test.py

建立 pyc 檔案

一個 pyc 檔案包含三部分獨立的資訊:

  • magic number
  • pyc 檔案的建立時間資訊
  • PyCodeObject

import.c

下面一一進行說明

1,magic number
是 Python 定義的一個整數值,不同版本定義不同,用來確保 Python 的相容性。Python 在載入 pyc 時首先檢查 magic number ,如果與 Python 自身的 magic number 不同,說明建立 pyc 的 Python 版本 與 當前版本不相容,會拒絕載入。

為什麼會不相容呢?因為位元組碼指令發生了變化,有刪除或增加。

2,pyc 建立時間
使得 Python 自動將 pyc 檔案與最新的 Python 檔案同步。當對 Python 程式進行編譯產生 pyc 後,如果後來進行了修改,此時 Python 在嘗試載入 pyc 時,會發現 pyc 建立時間早於 Python 程式,於是將重新編譯,生成新的 pyc 檔案。

3,PyCodeObject
編譯器會遍歷 PyCodeObject 中的所有域,並依次寫入 pyc。對於 PyCodeObject 中的每一個物件,同樣會進行遍歷,並寫入型別標誌資料(數值/字串)

型別標誌的三個作用:表明上一個物件的結束、新物件的開始、確定新物件的型別

marshal.h,型別標誌

向 pyc 寫入字串

部分略

對於巢狀的名字空間,產生的 PyCodeObject 也是遞迴巢狀的,巢狀的 PyCodeObject 在上層 PyCodeObject 的co_consts中。

位元組碼

原始碼編譯為 位元組碼指令 序列,虛擬機器根據位元組碼進行操作,完成程式的執行,opcode.h中定義了當前版本 Python 支援的位元組碼指令。

位元組碼指令 的編碼並不是按順序增長的,中間有跳躍。

Include目錄下的opcode.h定義了位元組碼指令

解析 pyc

由於包含巢狀 PyCodeObject,pyc 中的二進位制資料實際上是有結構的,可以以 XML格式進行解析,從而視覺化。使用 pycparser。

而 Python 庫中 dis 的 dis 方法可以對 code物件 進行解析。接收 code物件,輸出 位元組碼指令資訊。

dis.dis 的輸出:

  • 第一列,是 位元組碼指令 對應的 原始碼 在 Python 程式中的行數
  • 第二列,是當前 位元組碼指令 在 co_code 中的偏移位置
  • 第三列,當前的位元組碼指令
  • 第四列,當前位元組碼指令的引數

test.py

參考資料

《Python 原始碼剖析》第七章

相關文章