python深入系列(一):python源碼運行流程

源碼編譯

要看懂源碼,肯定要學會編譯源碼。先去官網下載源碼文件,然后解壓編譯安裝。linux環境下進解壓目錄依次運行:

  • ./configure -prefix=<安裝路徑>
  • make
  • make install

至此一個可運行python就已經編譯完成,之后便可以愉快地改源碼了。

python運行流程

當我們運行python命令時,代碼流向是怎樣的呢?makefile中target指向Modules文件夾的python.c,
執行里面的Py_Main方法,Py_Main方法實現在main.c,主要是判斷命令參數執行不同操作,整個方法可以簡化成初始化環境、執行、清理環境:

Py_Initialize();
sts = PyRun_AnyFileExFlags(
                    fp,
                    filename == NULL ? "<stdin>" : filename,
                    filename != NULL, &cf) != 0;
            }
Py_Finalize();

python有兩種運行模式,命令行模式和文件執行,最終都會調用PyRun_AnyFileFlags方法,參數里面的filename就對應著運行python abc.py這種模式。
PyRun_AnyFileFlags在Python目錄的pythonrun.c:

int
PyRun_AnyFileExFlags(FILE *fp, const char *filename, int closeit,
                     PyCompilerFlags *flags)
{
    if (filename == NULL)
        filename = "???";
    if (Py_FdIsInteractive(fp, filename)) {
        int err = PyRun_InteractiveLoopFlags(fp, filename, flags);
        if (closeit)
            fclose(fp);
        return err;
    }
    else
        return PyRun_SimpleFileExFlags(fp, filename, closeit, flags);
}

PyRun_InteractiveLoopFlags和PyRun_SimpleFileExFlags分別對應交互式命令行執行和文件執行,但兩者最終執行流程會匯合在一起。
我們先來看下Py_Initialize方法,初始化環境初始化了些什么。

python運行環境初始化

Py_Initialize也在pythonrun.c里,實際調用了Py_InitializeEx方法,初始化順序依次如下:

  • hash算法 隨機數初始化
_PyRandom_Init();

這里只是初始化hash算法需要用到的隨機數環境,使得 dict 對象中 key 的順序每次啟動時隨機;而python的random模塊在Lib文件夾的random.py,random.py內部引用的是C生成python模塊,位于Modules的_randommodule.c,其中一個隨機數基礎方法random_random挺有意思,對應random(),把注釋也貼出來:

/* random_random is the function named genrand_res53 in the original code;
 * generates a random number on [0,1) with 53-bit resolution; note that
 * 9007199254740992 == 2**53; I assume they're spelling "/2**53" as
 * multiply-by-reciprocal in the (likely vain) hope that the compiler will
 * optimize the division away at compile-time.  67108864 is 2**26.  In
 * effect, a contains 27 random bits shifted left 26, and b fills in the
 * lower 26 bits of the 53-bit numerator.
 * The orginal code credited Isaku Wada for this algorithm, 2002/01/09.
 */
static PyObject *
random_random(RandomObject *self)
{
    unsigned long a=genrand_int32(self)>>5, b=genrand_int32(self)>>6;
    return PyFloat_FromDouble((a*67108864.0+b)*(1.0/9007199254740992.0));
}

PyFloat_FromDouble表示從double類型轉換成PyFloat對象,其中genrand_int32方法也是這種魔數風格,感覺很geek。

  • 進程環境初始化
interp = PyInterpreterState_New();

這里只是初始化一個進程狀態鏈表,進程實現在Lib文件夾的multiprocessing和Modules文件夾的_multiprocessing,底層使用的是系統的進程,根據系統不同實現不同

  • 線程環境初始化
tstate = PyThreadState_New(interp);

這里只是初始化一個線程狀態鏈表,線程實現在Modules的threadmodule.c,底層使用的是系統的線程,根據系統不同實現不同

  • 類型初始化
_Py_ReadyTypes();

里面關鍵的方法PyType_Ready實現在Objects的typeobject.c,根據類型不同進行了初始化

  • int、long等對象初始化
_PyFrame_Init()
_PyInt_Init()
_PyLong_Init()
PyByteArray_Init()
_PyFloat_Init()
_PyUnicode_Init()

除了_PyFrame_Init代表builtin_object,其他方法看名字應該就清楚其用途

  • __builtin__ module初始化
bimod = _PyBuiltin_Init();

點進去就會發現都是些熟悉的面孔

  • sys module初始化
sysmod = _PySys_Init();
  • 初始化import環境
_PyImport_Init();
  • 初始化內建異常
_PyExc_Init();
  • 在sys module中添加一些對象用于import
_PyImportHooks_Init();
  • 初始化信號機制
initsigs();
  • 初始化warning機制
_PyWarnings_Init();
  • 初始化main
 initmain();
  • 這個應該和GIL有關,具體要看下GIL
_PyGILState_Init(interp, tstate);
  • site module初始化
initsite();

就是import Lib文件夾里site.py,目的是加載site-packages里的包,不同平臺情況不同

字節碼運行

初始完環境,就可以開始執行自己寫的代碼了。之前提到PyRun_InteractiveLoopFlags和PyRun_SimpleFileExFlags兩者最終執行流程會匯合在一起,這里把兩者代碼簡化一下:
PyRun_InteractiveLoopFlags

for (;;) {
        ret = PyRun_InteractiveOneFlags(fp, filename, flags);
        _PyDebug_PrintTotalRefs();
        if (ret == E_EOF)
            return 0;
    }

內部PyRun_InteractiveOneFlags方法

arena = PyArena_New();
mod = PyParser_ASTFromFile(fp, filename,
                               Py_single_input, ps1, ps2,
                               flags, &errcode, arena);
 m = PyImport_AddModule("__main__");
d = PyModule_GetDict(m);
v = run_mod(mod, filename, d, d, flags, arena);
PyArena_Free(arena);

PyRun_SimpleFileExFlags

m = PyImport_AddModule("__main__");
d = PyModule_GetDict(m);
if (maybe_pyc_file(fp, filename, ext, closeit)) {
    v = run_pyc_file(fp, filename, d, d, flags);
} else {
        v = PyRun_FileExFlags(fp, filename, Py_file_input, d, d,
                              closeit, flags);
}

內部run_pyc_file方法

magic = PyMarshal_ReadLongFromFile(fp);
if (magic != PyImport_GetMagicNumber()) {
    PyErr_SetString(PyExc_RuntimeError,
    "Bad magic number in .pyc file");
    return NULL;
}
(void) PyMarshal_ReadLongFromFile(fp);
v = PyMarshal_ReadLastObjectFromFile(fp);
co = (PyCodeObject *)v;
v = PyEval_EvalCode(co, globals, locals);

內部PyRun_FileExFlags方法

PyArena *arena = PyArena_New();
mod = PyParser_ASTFromFile(fp, filename, start, 0, 0,
                               flags, NULL, arena);
ret = run_mod(mod, filename, globals, locals, flags, arena);
PyArena_Free(arena);

看了代碼,可以發現只有兩種模式:根據pyc文件執行和調用run_mod方法。pyc概念就不多說了,pyc文件開頭會記錄magic數,用于兼容python版本問題;而調用run_mod之前的arena是python的內存管理單位,run_mod的代碼如下:

co = PyAST_Compile(mod, filename, flags, arena);
v = PyEval_EvalCode(co, globals, locals);

這樣所有的執行都匯合到了一起,調用PyEval_EvalCode方法。PyEval_EvalCode接收一個PyCodeObject對象,PyCodeObject對象是python代碼編譯結果,由此可見pyc文件其實是個PyCodeObject緩存,pyc從名字看也可以看出是簡稱嘛。PyEval_EvalCode內部調用了PyEval_EvalFrameEx方法,PyEval_EvalFrameEx就是最后的大魔王了。PyEval_EvalFrameEx是個很龐大的方法,還是先從PyCodeObject入手,使用dis模塊就可以知道PyCodeObject的結構了,比如print('hello,world')的輸出如下:

('const : ', ('hello,world', None))
('name : ', ())
  2           0 LOAD_CONST               0 ('hello,world')
              3 PRINT_ITEM          
              4 PRINT_NEWLINE       
              5 LOAD_CONST               1 (None)
              8 RETURN_VALUE        

第一個是靜態變量,第二個是變量,第三個是字節碼,這個字節碼就是執行的核心。PyEval_EvalFrameEx中有個大switch塊,里面的case條件就對應著字節碼(所有的字節碼在這個篇文章可以看到)。

最后的Py_Finalize方法可以看成是Py_Initialize反過程。

本文到這里就結束了,python源碼內容豐富龐大,這篇文章只是淺顯地過了一下,目的是了解代碼執行原理、了解整個脈絡,具體細節可以參考《Python源碼剖析》這本書。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 今天臨時受到朋友的邀請,參加了一場頭馬英語俱樂部的活動,全場純英文交流。氣氛很好,融入很快。 兩個小時的活動結束后...
    黃油泡菜閱讀 444評論 0 0
  • 子、丑、寅、卯、辰、巳、午、未、申、酉、戌、亥的總稱。又稱十二支。中國古代用十二地支紀時、紀月。地支紀時就是將一日...
    金馬玉堂張奇閱讀 1,421評論 0 2
  • 1. javascript刪除元素節點 IE中有這樣一個方法:removeNode(),這個方法在IE下是好使的,...
    落魂灬閱讀 344評論 0 1