Python編譯結果之code對象與pyc文件詳解

1、Python程序執行過程

與java類似,Python將.py編譯為字節碼,然後通過虛擬機執行。編譯過程與虛擬機執行過程均在python25.dll中。Python虛擬機比java更抽象,離底層更遠。

編譯過程不僅生成字節碼,還要包含常量、變量、占用棧的空間等,Pyton中編譯過程生成code對象PyCodeObject。將PyCodeObject寫入二進制文件,即.pyc。

有必要則寫入A.pyc指的是該.py是否隻運行一次,如果import的模塊,肯定會生成.pyc。

2、PyCodeObject對象與.pyc文件

Python解釋器將.py程序編譯為PyCodeObject對象,具體過程與編譯原理類似。

typedef struct {
    PyObject_HEAD
    int co_argcount;		// Code Block的參數的個數,比如說一個函數的參數
    int co_nlocals;			// Code Block中局部變量的個數
    int co_stacksize;		// 執行該段Code Block需要的棧空間
    int co_flags;			// N/A
    PyObject *co_code;		// Code Block編譯所得的byte code,以PyStringObject的形式存在
    PyObject *co_consts;	// PyTupleObject對象,保存Code Block中的常量
    PyObject *co_names;		// PyTupleObject對象,保存Code Block中的所有符號
    PyObject *co_varnames;	// Code Block中局部變量名集合
    PyObject *co_freevars;	// 實現閉包所需東西
    PyObject *co_cellvars;  // Code Block內部嵌套函數所引用的局部變量名集合
    PyObject *co_filename;	// Code Block所對應的.py文件的完整路徑
    PyObject *co_name;		// Code Block的名字,通常是函數名或類名
    int co_firstlineno;		// Code Block在對應的.py文件中的起始行
    PyObject *co_lnotab;	// byte code與.py文件中source code行號的對應關系,以PyStringObject的形式存在
    void *co_zombieframe;
    PyObject *co_weakreflist;
} PyCodeObject;

一個Code Block生成一個PyCodeObject,進入一個名字空間成為進入一個Code Block。如下.py文件編譯完成後會生成三個PyCodeObject,一個對應整個.py文件一個對應Class A,一個對應def Fun。實際這三個code對象是嵌套的,後兩個code對象位於第一個code對象的co_consts屬性中。其實,字節碼位於co_code中。

class A:
	pass
def Fun():
	pass
a = A()
Fun()

pyc文件包括三部分:

(1)四字節的Magic int,表示pyc版本信息

(2)四字節的int,是pyc產生時間,若與py文件時間不同會重新生成

(3)序列化瞭的PyCodeObject對象。

3、pyc文件的生成

寫入pyc文件的函數包括以下幾個步驟:

PyMarshal_WriteLongToFile(pyc_magic, fp, Py_MARSHAL_VERSION);	// 寫入版本信息
PyMarshal_WriteLongToFile(0L, fp, Py_MARSHAL_VERSION);			// 寫入時間信息
PyMarshal_WriteObjectToFile((PyObject *)co, fp, Py_MARSHAL_VERSION);	// 寫入PyCodeObject對象

關鍵在於code對象的寫入:

{
    WFILE wf;
    wf.fp = fp;
	……
    w_object(x, &wf);
}

用到瞭一個WFILE結構體,可以認為是對FILE *fp 的一個封裝:

typedef struct {
    FILE *fp;
    int error;
    int depth;
    PyObject *strings; // 存儲字符串,寫入時以dict形式,讀出時以list形式
} WFILE;

關鍵在於w_object()函數:

static void w_object(PyObject *v, WFILE *p){
	if (v == NULL)	……
	else if (PyInt_CheckExact(v)) ……
	else if (PyFloat_CheckExact(v)) ……
	else if (PyString_CheckExact(v)) ……
	else if (PyList_CheckExact(v)) ……
}

w_code實質為根據不同的對象類型選取不同的策略,例如tuple對象:

    else if (PyTuple_CheckExact(v)) {
        w_byte(TYPE_TUPLE, p);
        n = PyTuple_Size(v);
        W_SIZE(n, p);
        for (i = 0; i < n; i++) 
            w_object(PyTuple_GET_ITEM(v, i), p);

而所有類型最終可分解為寫入數值與寫入字符串兩種操作,涉及以下幾部分:

#define w_byte(c, p) putc((c), (p)->fp)	// 用於寫入類型
static void w_long(long x, WFILE *p){	// 用於寫入數字	
    w_byte((char)( x      & 0xff), p);	// 實質為用四個字節存儲一個數字
    w_byte((char)((x>> 8) & 0xff), p);
    w_byte((char)((x>>16) & 0xff), p);
    w_byte((char)((x>>24) & 0xff), p);
}
static void w_string(char *s, int n, WFILE *p){	//用於寫入字符串
    fwrite(s, 1, n, p->fp);
}

由於序列化寫入文件後丟失瞭結構信息,故寫入每個對象時寫入類型信息w_byte:

#define TYPE_INT                'i'
#define TYPE_LIST               '['
#define TYPE_DICT               '{'
#define TYPE_CODE               'c'

由於Python皆對象,w_object(PyObject*)便可針對不同類型選取不同寫入方法,不斷細分,最終分解為PyInt_Object或PyString_Object,利用w_long或w_string寫入。

數字比較簡單:

else if (PyInt_CheckExact(v)) {
    w_byte(TYPE_INT, p);
    w_long(x, p);
}

字符串則比較復雜:

 else if (PyString_CheckExact(v)) {
        if (p->strings && PyString_CHECK_INTERNED(v)) {
            PyObject *o = PyDict_GetItem(p->strings, v);	// 獲取在strings中的序號
            if (o) {			// inter對象的非首次寫入
                long w = PyInt_AsLong(o);
                w_byte(TYPE_STRINGREF, p);
                w_long(w, p);
                goto exit;
            }
            else {				// intern對象的首次寫入
                int ok;
                ok = o && PyDict_SetItem(p->strings, v, o) >= 0;
                Py_XDECREF(o);
                w_byte(TYPE_INTERNED, p);
            }
        }
        else {					// 寫入普通string
            w_byte(TYPE_STRING, p);
        }
        n = PyString_GET_SIZE(v);
        W_SIZE(n, p);
        w_string(PyString_AS_STRING(v), n, p);
    }		

(1)若寫入普通字符串,寫入字符串類型信息”S”,然後寫入字符串長度及string值。

(2)若寫入inter字符串,先到WFILE的strings中查找:

(a)若找到,則寫入引用類型信息”R”,然後寫入序號

(b)若未找到,創建對象放入strings,並寫入intern類型信息”t”,然後寫入字符串長度及string值。
若依次寫入”efei”、”snow”、”efei”,則會如下:

從pyc文件讀入時,依靠list,那麼序號就可以利用上瞭。

總結

本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!

推薦閱讀: