前面我們提到,每一個代碼塊(code block)都會對應(yīng)一個PyCodeObject對象,Python會將該對象存儲在pyc文件中。但不幸的是,事實并不總是這樣。有時,當我們運行一個簡單的程序時并沒有產(chǎn)生pyc文件,因此我們猜測:有些Python程序只是臨時完成一些瑣碎的工作,這樣的程序僅僅只會運行一次,然后就不會再使用了,因此也就沒有保存至pyc文件的必要。 如果我們在代碼中加上了一個import abc這樣的語句,再執(zhí)行你就會發(fā)現(xiàn)Python為其生成了pyc文件,這就說明import會觸發(fā)pyc的生成。 實際上,在運行過程中,如果碰到import abc這樣的語句,那么Python會在設(shè)定好的path中尋找abc.pyc或者abc.pyd文件。如果沒有這些文件,而是只發(fā)現(xiàn)了abc.py,那么Python會先將abc.py編譯成PyCodeObject,然后創(chuàng)建pyc文件,并將PyCodeObject寫到pyc文件里面去。 接下來,再對abc.pyc進行import動作,對,并不是編譯成PyCodeObject對象之后就直接使用。而是先寫到pyc文件里面去,然后再將pyc文件里面的PyCodeObject對象重新在內(nèi)存中復(fù)制出來。 關(guān)于Python的import機制,我們后面會剖析,這里只是用來完成pyc文件的觸發(fā)。當然得到pyc文件還有其它方法,比如使用py_compile模塊。 # a.py class A: a = 1 # b.py import a
執(zhí)行b.py的時候,會發(fā)現(xiàn)創(chuàng)建了a.cpython-38.pyc。另外關(guān)于pyc文件的創(chuàng)建位置,會在當前文件的同級目錄下的__pycache__目錄中創(chuàng)建,名字就叫做:py文件名.cpython-版本號.pyc。
上面我們提到,Python通過import module進行加載時,如果沒有找到相應(yīng)的pyc或者pyd文件,就會在py文件的基礎(chǔ)上自動創(chuàng)建pyc文件。而創(chuàng)建之后,會往里面寫入三個內(nèi)容: 1. magic number 這是Python定義的一個整數(shù)值,不同版本的Python會定義不同的magic number,這個值是為了保證Python能夠加載正確的pyc。 比如Python3.7不會加載3.6版本的pyc,因為Python在加載pyc文件的時候會首先檢測該pyc的magic number,如果和自身的magic number不一致,則拒絕加載。
2. pyc的創(chuàng)建時間 這個很好理解,判斷源代碼的最后修改時間和pyc文件的創(chuàng)建時間。如果pyc文件的創(chuàng)建時間比源代碼的修改時間要早,說明在生成pyc之后,源代碼被修改了,那么會重新編譯并生成新的pyc,而反之則會直接加載已存在的pyc。 3. PyCodeObject對象 這個不用說了,肯定是要存儲的。
下面就來看看pyc文件是如何寫入上面三個內(nèi)容的。 既然要寫入,那么肯定要有文件句柄,我們來看看: //位置:Python/marshal.c
//FILE是 C 自帶的文件句柄 //可以把WFILE看成是FILE的包裝 typedef struct { FILE *fp; //文件句柄 //下面的字段在寫入信息的時候會看到 int error; int depth; PyObject *str; char *ptr; char *end; char *buf; _Py_hashtable_t *hashtable; int version; } WFILE;
首先是寫入magic number和創(chuàng)建時間,它們會調(diào)用PyMarshal_WriteLongToFile函數(shù)進行寫入: void PyMarshal_WriteLongToFile(long x, FILE *fp, int version) { //magic number和創(chuàng)建時間,只是一個整數(shù) //在寫入的時候,使用char [4]來保存 char buf[4]; //聲明一個WFILE類型變量wf WFILE wf; //內(nèi)存初始化 memset(&wf, 0, sizeof(wf)); //初始化內(nèi)部成員 wf.fp = fp; wf.ptr = wf.buf = buf; wf.end = wf.ptr + sizeof(buf); wf.error = WFERR_OK; wf.version = version; //調(diào)用w_long將x、也就是版本信息或者時間寫到wf里面去 w_long(x, &wf); //刷到磁盤上 w_flush(&wf); }
所以該函數(shù)只是初始化了一個WFILE對象,真正寫入則是調(diào)用的w_long。
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); }
w_long則是調(diào)用 w_byte 將 x 逐個字節(jié)地寫到文件里面去。 而寫入PyCodeObject對象則是調(diào)用了PyMarshal_WriteObjectToFile,我們也來看看長什么樣子。 void PyMarshal_WriteObjectToFile(PyObject *x, FILE *fp, int version) { char buf[BUFSIZ]; WFILE wf; memset(&wf, 0, sizeof(wf)); wf.fp = fp; wf.ptr = wf.buf = buf; wf.end = wf.ptr + sizeof(buf); wf.error = WFERR_OK; wf.version = version; if (w_init_refs(&wf, version)) return; /* caller mush check PyErr_Occurred() */ w_object(x, &wf); w_clear_refs(&wf); w_flush(&wf); }
可以看到和PyMarshal_WriteLongToFile基本是類似的,只不過在實際寫入的時候,PyMarshal_WriteLongToFile調(diào)用的是w_long,而PyMarshal_WriteObjectToFile調(diào)用的是w_object。 static void w_object(PyObject *v, WFILE *p) { char flag = '\0';
p->depth++;
if (p->depth > MAX_MARSHAL_STACK_DEPTH) { p->error = WFERR_NESTEDTOODEEP; } else if (v == NULL) { w_byte(TYPE_NULL, p); } else if (v == Py_None) { w_byte(TYPE_NONE, p); } else if (v == PyExc_StopIteration) { w_byte(TYPE_STOPITER, p); } else if (v == Py_Ellipsis) { w_byte(TYPE_ELLIPSIS, p); } else if (v == Py_False) { w_byte(TYPE_FALSE, p); } else if (v == Py_True) { w_byte(TYPE_TRUE, p); } else if (!w_ref(v, &flag, p)) w_complex_object(v, flag, p);
p->depth--; }
可以看到本質(zhì)上還是調(diào)用了w_byte,但這僅僅是一些特殊的對象。如果是列表、字典之類的數(shù)據(jù),那么會調(diào)用w_complex_object,也就是代碼中的最后一個else if分支。 w_complex_object這個函數(shù)的源代碼很長,我們看一下整體結(jié)構(gòu),具體邏輯就不貼了,我們后面會單獨截取一部分進行分析。 static void w_complex_object(PyObject *v, char flag, WFILE *p) { Py_ssize_t i, n; //如果是整數(shù)的話,執(zhí)行整數(shù)的寫入邏輯 if (PyLong_CheckExact(v)) { //...... } //如果是浮點數(shù)的話,執(zhí)行浮點數(shù)的寫入邏輯 else if (PyFloat_CheckExact(v)) { if (p->version > 1) { //...... } else { //...... } } //如果是復(fù)數(shù)的話,執(zhí)行復(fù)數(shù)的寫入邏輯 else if (PyComplex_CheckExact(v)) { if (p->version > 1) { //...... } else { //...... } } //如果是字節(jié)序列的話,執(zhí)行字節(jié)序列的寫入邏輯 else if (PyBytes_CheckExact(v)) { //...... } //如果是字符串的話,執(zhí)行字符串的寫入邏輯 else if (PyUnicode_CheckExact(v)) { if (p->version >= 4 && PyUnicode_IS_ASCII(v)) { //...... } else { //...... } } else { //...... } } //如果是元組的話,執(zhí)行元組的寫入邏輯 else if (PyTuple_CheckExact(v)) { //...... } //如果是列表的話,執(zhí)行列表的寫入邏輯 else if (PyList_CheckExact(v)) { //...... } //如果是字典的話,執(zhí)行字典的寫入邏輯 else if (PyDict_CheckExact(v)) { //...... } //如果是集合的話,執(zhí)行集合的寫入邏輯 else if (PyAnySet_CheckExact(v)) { //...... } //如果是PyCodeObject對象的話 //執(zhí)行PyCodeObject對象的寫入邏輯 else if (PyCode_Check(v)) { //...... } //如果是Buffer的話,執(zhí)行Buffer的寫入邏輯 else if (PyObject_CheckBuffer(v)) { //...... } else { W_TYPE(TYPE_UNKNOWN, p); p->error = WFERR_UNMARSHALLABLE; } }
源代碼雖然長,但是邏輯非常單純,就是對不同的對象、執(zhí)行不同的寫動作,然而其最終目的都是通過w_byte寫到pyc文件中。了解完函數(shù)的整體結(jié)構(gòu)之后,我們再看一下具體細節(jié),看看它在寫入對象的時候到底寫入了哪些內(nèi)容? static void w_complex_object(PyObject *v, char flag, WFILE *p) { //...... else if (PyList_CheckExact(v)) { W_TYPE(TYPE_LIST, p); n = PyList_GET_SIZE(v); W_SIZE(n, p); for (i = 0; i < n; i++) { w_object(PyList_GET_ITEM(v, i), p); } } else if (PyDict_CheckExact(v)) { Py_ssize_t pos; PyObject *key, *value; W_TYPE(TYPE_DICT, p); /* This one is NULL object terminated! */ pos = 0; while (PyDict_Next(v, &pos, &key, &value)) { w_object(key, p); w_object(value, p); } w_object((PyObject *)NULL, p); } //...... }
以列表和字典為例,它們在寫入的時候?qū)嶋H上寫的是內(nèi)部的元素,其它對象也是類似的。 def foo(): lst = [1, 2, 3]
# 把列表內(nèi)的元素寫進去了 print( foo.__code__.co_consts ) # (None, 1, 2, 3)
但問題來了,如果只是寫入元素的話,那么Python在加載的時候怎么知道它是一個列表呢?所以在寫入的時候不能光寫數(shù)據(jù),類型信息也要寫進去。我們再看一下上面列表和字典的寫入邏輯,里面都調(diào)用了W_TYPE,它負責(zé)將類型信息寫進去。 因此無論對于哪種對象,在寫入具體數(shù)據(jù)之前,都會先調(diào)用W_TYPE將類型信息寫進去。如果沒有類型信息,那么當Python加載pyc文件的時候,只會得到一坨字節(jié)流,而無法解析字節(jié)流中隱藏的結(jié)構(gòu)和蘊含的信息。 所以在往pyc文件里寫入數(shù)據(jù)之前,必須先寫入一個標識,諸如TYPE_LIST、TYPE_TUPLE、TYPE_DICT等等,這些標識正是對應(yīng)的類型信息。 如果解釋器在pyc文件中發(fā)現(xiàn)了這樣的標識,則預(yù)示著上一個對象結(jié)束,新的對象開始,并且也知道新對象是什么樣的對象,從而也知道該執(zhí)行什么樣的構(gòu)建動作。當然,這些標識也是可以看到的,在底層已經(jīng)定義好了。
//marshal.c #define TYPE_NULL '0' #define TYPE_NONE 'N' #define TYPE_FALSE 'F' #define TYPE_TRUE 'T' #define TYPE_STOPITER 'S' #define TYPE_ELLIPSIS '.' #define TYPE_INT 'i' /* TYPE_INT64 is not generated anymore. Supported for backward compatibility only. */ #define TYPE_INT64 'I' #define TYPE_FLOAT 'f' #define TYPE_BINARY_FLOAT 'g' #define TYPE_COMPLEX 'x' #define TYPE_BINARY_COMPLEX 'y' #define TYPE_LONG 'l' #define TYPE_STRING 's' #define TYPE_INTERNED 't' #define TYPE_REF 'r' #define TYPE_TUPLE '(' #define TYPE_LIST '[' #define TYPE_DICT '{' #define TYPE_CODE 'c' #define TYPE_UNICODE 'u' #define TYPE_UNKNOWN '?' #define TYPE_SET '<' #define TYPE_FROZENSET '>'
到了這里可以看到,其實Python對PyCodeObject對象的導(dǎo)出實際上是不復(fù)雜的。因為不管什么對象,最后都為歸結(jié)為兩種簡單的形式,一種是數(shù)值寫入,一種是字符串寫入。 上面都是對數(shù)值的寫入,比較簡單,僅僅需要按照字節(jié)依次寫入pyc即可。然而在寫入字符串的時候,Python設(shè)計了一種比較復(fù)雜的機制,有興趣可以自己閱讀源碼,這里不再介紹。
有下面一個文件:
class A: pass
def foo(): pass
顯然編譯之后會創(chuàng)建三個PyCodeObject對象,但是有兩個PyCodeObject對象是位于另一個PyCodeObject對象當中的。 也就是foo和A對應(yīng)的PyCodeObject對象,位于模塊對應(yīng)的PyCodeObject對象當中,準確的說是位于co_consts指向的常量池當中。舉個栗子: def f1(): def f2(): pass pass
print( f1.__code__.co_consts ) # (None, <code object f2 ...>, 'f1.<locals>.f2')
我們看到f2對應(yīng)的PyCodeObject確實位于f1的常量池當中,準確的說是f1的常量池中有一個指針指向f2對應(yīng)的PyCodeObject。 不過這都不是重點,重點是PyCodeObject對象是可以嵌套的。當在一個作用域內(nèi)部發(fā)現(xiàn)了一個新的作用域,那么新的作用域?qū)?yīng)的PyCodeObject對象會位于外層作用域的PyCodeObject對象的常量池中,或者說被常量池中的一個指針指向。
而在寫入pyc的時候會從最外層、也就是模塊的PyCodeObject對象開始寫入。如果碰到了包含的另一個PyCodeObject對象,那么就會遞歸地執(zhí)行寫入新的PyCodeObject對象。 如此下去,最終所有的PyCodeObject對象都會寫入到pyc文件當中。因此pyc文件里的PyCodeObject對象也是以一種嵌套的關(guān)系聯(lián)系在一起的,和代碼塊之間的關(guān)系是保持一致的。 def foo(): pass
def bar(): pass class A: def foo(self): pass
def bar(self): pass
這里問一下,上面那段代碼中創(chuàng)建了幾個PyCodeObject對象呢? 答案是6個,首先模塊是一個,foo函數(shù)一個,bar函數(shù)一個,類A一個,類A里面的foo函數(shù)一個,類A里面的bar函數(shù)一個,所以一共是6個。 而且這里的PyCodeObject對象是層層嵌套的,一開始是對整個全局模塊創(chuàng)建PyCodeObject對象,然后遇到了函數(shù)foo,那么再為函數(shù)foo創(chuàng)建PyCodeObject對象,依次往下。 所以,如果是常量值,則相當于是靜態(tài)信息,直接存儲起來便可??扇绻呛瘮?shù)、類,那么會為其創(chuàng)建新的PyCodeObject對象,然后再收集起來。
以上就是pyc文件相關(guān)的內(nèi)容,源文件在編譯之后會得到pyc文件。因此我們不光可以手動導(dǎo)入 pyc,用Python直接執(zhí)行pyc文件也是可以的。
|