一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

《源碼探秘 CPython》45. pyc文件是怎么創(chuàng)建的?

 古明地覺O_o 2022-12-08 發(fā)布于北京

一念之間,就有可能失去全部


pyc文件的觸發(fā)



前面我們提到,每一個代碼塊(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.pyclass A:    a = 1    # b.pyimport a

執(zhí)行b.py的時候,會發(fā)現(xiàn)創(chuàng)建了a.cpython-38.pyc。另外關(guān)于pyc文件的創(chuàng)建位置,會在當前文件的同級目錄下的__pycache__目錄中創(chuàng)建,名字就叫做:py文件名.cpython-版本號.pyc。


pyc文件里面包含哪些內(nèi)容



上面我們提到,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文件的寫入



下面就來看看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ù)進行寫入:

voidPyMarshal_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 voidw_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,我們也來看看長什么樣子。

voidPyMarshal_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 voidw_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 voidw_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 voidw_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ù)雜的機制,有興趣可以自己閱讀源碼,這里不再介紹。


PyCodeObject的包含關(guān)系



有下面一個文件:

class A:    pass
def foo(): pass

顯然編譯之后會創(chuàng)建三個PyCodeObject對象,但是有兩個PyCodeObject對象是位于另一個PyCodeObject對象當中的。

也就是fooA對應(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對象,然后再收集起來。


小結(jié)



以上就是pyc文件相關(guān)的內(nèi)容,源文件在編譯之后會得到pyc文件。因此我們不光可以手動導(dǎo)入 pyc,用Python直接執(zhí)行pyc文件也是可以的。

    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    国产在线日韩精品欧美| 欧美国产日产在线观看| 国产午夜福利在线观看精品| 亚洲一区二区三区熟女少妇| 好吊妞视频这里有精品| 日韩欧美精品一区二区三区| 亚洲一区精品二人人爽久久| 午夜色午夜视频之日本| 久久99一本色道亚洲精品| 成人午夜爽爽爽免费视频| 亚洲香艳网久久五月婷婷| 日韩欧美好看的剧情片免费| 亚洲国产av在线视频| 91偷拍视频久久精品| 欧美成人欧美一级乱黄| 日本人妻丰满熟妇久久| 大香蕉再在线大香蕉再在线| 自拍偷拍一区二区三区| 色丁香一区二区黑人巨大| 国产超薄黑色肉色丝袜| 99免费人成看国产片| 欧美成人免费夜夜黄啪啪| 国产又爽又猛又粗又色对黄| 中文字幕中文字幕一区二区| 亚洲精品一区三区三区| 空之色水之色在线播放| 欧美精品在线播放一区二区| 亚洲中文字幕高清乱码毛片| 视频在线播放你懂的一区| 欧美成人黄色一级视频| 在线中文字幕亚洲欧美一区| 青青草草免费在线视频| 欧洲一级片一区二区三区| 人妻内射在线二区一区| 男女午夜在线免费观看视频| 欧美大胆美女a级视频| 国产欧美性成人精品午夜| 国产又粗又猛又黄又爽视频免费| 暴力三级a特黄在线观看| 欧美性欧美一区二区三区| 亚洲av秘片一区二区三区|