pyc文件的觸發
前面我們提到,每一個代碼塊(code block)都會對應一個PyCodeObject對象,Python會將該對象存儲在pyc文件中。但不幸的是,事實並不總是這樣。有時,當我們運行一個簡單的程序時並沒有產生pyc文件,因此我們猜測:有些Python程序只是臨時完成一些瑣碎的工作,這樣的程序僅僅只會運行一次,然後就不會再使用了,因此也就沒有保存至pyc文件的必要。
如果我們在代碼中加上了一個import abc這樣的語句,再執行你就會發現Python爲其生成了pyc文件,這就說明import會觸發pyc的生成。
實際上,在運行過程中,如果碰到import abc這樣的語句,那麼Python會在設定好的path中尋找abc.pyc或者abc.pyd文件。如果沒有這些文件,而是隻發現了abc.py,那麼Python會先將abc.py編譯成PyCodeObject,然後創建pyc文件,並將PyCodeObject寫到pyc文件裏面去。
接下來,再對abc.pyc進行import動作,對,並不是編譯成PyCodeObject對象之後就直接使用。而是先寫到pyc文件裏面去,然後再將pyc文件裏面的PyCodeObject對象重新在內存中複製出來。
關於Python的import機制,我們後面會剖析,這裏只是用來完成pyc文件的觸發。當然得到pyc文件還有其它方法,比如使用py_compile模塊。
# a.py class A: a = 1 # b.py import a
執行b.py的時候,會發現創建了a.cpython-38.pyc。另外關於pyc文件的創建位置,會在當前文件的同級目錄下的__pycache__目錄中創建,名字就叫做:py文件名.cpython-版本號.pyc。
pyc文件裏面包含哪些內容
上面我們提到,Python通過import module進行加載時,如果沒有找到相應的pyc或者pyd文件,就會在py文件的基礎上自動創建pyc文件。而創建之後,會往裏面寫入三個內容:
1. magic number
這是Python定義的一個整數值,不同版本的Python會定義不同的magic number,這個值是爲了保證Python能夠加載正確的pyc。
比如Python3.7不會加載3.6版本的pyc,因爲Python在加載pyc文件的時候會首先檢測該pyc的magic number,如果和自身的magic number不一致,則拒絕加載。
2. pyc的創建時間
這個很好理解,判斷源代碼的最後修改時間和pyc文件的創建時間。如果pyc文件的創建時間比源代碼的修改時間要早,說明在生成pyc之後,源代碼被修改了,那麼會重新編譯並生成新的pyc,而反之則會直接加載已存在的pyc。
3. PyCodeObject對象
這個不用說了,肯定是要存儲的。
pyc文件的寫入
下面就來看看pyc文件是如何寫入上面三個內容的。
既然要寫入,那麼肯定要有文件句柄,我們來看看:
//位置: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和創建時間,它們會調用PyMarshal_WriteLongToFile函數進行寫入:
void PyMarshal_WriteLongToFile(long x, FILE *fp, int version) { //magic number和創建時間,只是一個整數 //在寫入的時候,使用char [4]來保存 char buf[4]; //聲明一個WFILE類型變量wf 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; //調用w_long將x、也就是版本信息或者時間寫到wf裏面去 w_long(x, &wf); //刷到磁盤上 w_flush(&wf); }
所以該函數只是初始化了一個WFILE對象,真正寫入則是調用的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則是調用 w_byte 將 x 逐個字節地寫到文件裏面去。
而寫入PyCodeObject對象則是調用了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調用的是w_long,而PyMarshal_WriteObjectToFile調用的是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--; }
可以看到本質上還是調用了w_byte,但這僅僅是一些特殊的對象。如果是列表、字典之類的數據,那麼會調用w_complex_object,也就是代碼中的最後一個else if分支。
w_complex_object這個函數的源代碼很長,我們看一下整體結構,具體邏輯就不貼了,我們後面會單獨截取一部分進行分析。
static void w_complex_object(PyObject *v, char flag, WFILE *p) { Py_ssize_t i, n; //如果是整數的話,執行整數的寫入邏輯 if (PyLong_CheckExact(v)) { //...... } //如果是浮點數的話,執行浮點數的寫入邏輯 else if (PyFloat_CheckExact(v)) { if (p->version > 1) { //...... } else { //...... } } //如果是複數的話,執行復數的寫入邏輯 else if (PyComplex_CheckExact(v)) { if (p->version > 1) { //...... } else { //...... } } //如果是字節序列的話,執行字節序列的寫入邏輯 else if (PyBytes_CheckExact(v)) { //...... } //如果是字符串的話,執行字符串的寫入邏輯 else if (PyUnicode_CheckExact(v)) { if (p->version >= 4 && PyUnicode_IS_ASCII(v)) { //...... } else { //...... } } else { //...... } } //如果是元組的話,執行元組的寫入邏輯 else if (PyTuple_CheckExact(v)) { //...... } //如果是列表的話,執行列表的寫入邏輯 else if (PyList_CheckExact(v)) { //...... } //如果是字典的話,執行字典的寫入邏輯 else if (PyDict_CheckExact(v)) { //...... } //如果是集合的話,執行集合的寫入邏輯 else if (PyAnySet_CheckExact(v)) { //...... } //如果是PyCodeObject對象的話 //執行PyCodeObject對象的寫入邏輯 else if (PyCode_Check(v)) { //...... } //如果是Buffer的話,執行Buffer的寫入邏輯 else if (PyObject_CheckBuffer(v)) { //...... } else { W_TYPE(TYPE_UNKNOWN, p); p->error = WFERR_UNMARSHALLABLE; } }
源代碼雖然長,但是邏輯非常單純,就是對不同的對象、執行不同的寫動作,然而其最終目的都是通過w_byte寫到pyc文件中。瞭解完函數的整體結構之後,我們再看一下具體細節,看看它在寫入對象的時候到底寫入了哪些內容?
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); } //...... }
以列表和字典爲例,它們在寫入的時候實際上寫的是內部的元素,其它對象也是類似的。
def foo(): lst = [1, 2, 3] # 把列表內的元素寫進去了 print( foo.__code__.co_consts ) # (None, 1, 2, 3)
但問題來了,如果只是寫入元素的話,那麼Python在加載的時候怎麼知道它是一個列表呢?所以在寫入的時候不能光寫數據,類型信息也要寫進去。我們再看一下上面列表和字典的寫入邏輯,裏面都調用了W_TYPE,它負責將類型信息寫進去。
因此無論對於哪種對象,在寫入具體數據之前,都會先調用W_TYPE將類型信息寫進去。如果沒有類型信息,那麼當Python加載pyc文件的時候,只會得到一坨字節流,而無法解析字節流中隱藏的結構和蘊含的信息。
所以在往pyc文件裏寫入數據之前,必須先寫入一個標識,諸如TYPE_LIST、TYPE_TUPLE、TYPE_DICT等等,這些標識正是對應的類型信息。
如果解釋器在pyc文件中發現了這樣的標識,則預示着上一個對象結束,新的對象開始,並且也知道新對象是什麼樣的對象,從而也知道該執行什麼樣的構建動作。當然,這些標識也是可以看到的,在底層已經定義好了。
//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對象的導出實際上是不復雜的。因爲不管什麼對象,最後都爲歸結爲兩種簡單的形式,一種是數值寫入,一種是字符串寫入。
上面都是對數值的寫入,比較簡單,僅僅需要按照字節依次寫入pyc即可。然而在寫入字符串的時候,Python設計了一種比較複雜的機制,有興趣可以自己閱讀源碼,這裏不再介紹。
PyCodeObject的包含關係
有下面一個文件:
class A: pass def foo(): pass
顯然編譯之後會創建三個PyCodeObject對象,但是有兩個PyCodeObject對象是位於另一個PyCodeObject對象當中的。
也就是foo和A對應的PyCodeObject對象,位於模塊對應的PyCodeObject對象當中,準確的說是位於co_consts指向的常量池當中。舉個栗子:
def f1(): def f2(): pass pass print( f1.__code__.co_consts ) # (None, <code object f2 ...>, 'f1.<locals>.f2')
我們看到f2對應的PyCodeObject確實位於f1的常量池當中,準確的說是f1的常量池中有一個指針指向f2對應的PyCodeObject。
不過這都不是重點,重點是PyCodeObject對象是可以嵌套的。當在一個作用域內部發現了一個新的作用域,那麼新的作用域對應的PyCodeObject對象會位於外層作用域的PyCodeObject對象的常量池中,或者說被常量池中的一個指針指向。
而在寫入pyc的時候會從最外層、也就是模塊的PyCodeObject對象開始寫入。如果碰到了包含的另一個PyCodeObject對象,那麼就會遞歸地執行寫入新的PyCodeObject對象。
如此下去,最終所有的PyCodeObject對象都會寫入到pyc文件當中。因此pyc文件裏的PyCodeObject對象也是以一種嵌套的關係聯繫在一起的,和代碼塊之間的關係是保持一致的。
def foo(): pass def bar(): pass class A: def foo(self): pass def bar(self): pass
這裏問一下,上面那段代碼中創建了幾個PyCodeObject對象呢?
答案是6個,首先模塊是一個,foo函數一個,bar函數一個,類A一個,類A裏面的foo函數一個,類A裏面的bar函數一個,所以一共是6個。
而且這裏的PyCodeObject對象是層層嵌套的,一開始是對整個全局模塊創建PyCodeObject對象,然後遇到了函數foo,那麼再爲函數foo創建PyCodeObject對象,依次往下。
所以,如果是常量值,則相當於是靜態信息,直接存儲起來便可。可如果是函數、類,那麼會爲其創建新的PyCodeObject對象,然後再收集起來。
小結
以上就是pyc文件相關的內容,源文件在編譯之後會得到pyc文件。因此我們不光可以手動導入 pyc,用Python直接執行pyc文件也是可以的。
以上就是本次分享的所有內容,想要了解更多歡迎前往公衆號:Python編程學習圈,每日干貨分享