1. 程式人生 > 實用技巧 >24. Python執行時環境的初始化、原始碼分析Python直譯器在啟動時都做了哪些事情?

24. Python執行時環境的初始化、原始碼分析Python直譯器在啟動時都做了哪些事情?

楔子

我們之前分析了Python的核心--位元組碼、以及虛擬機器的剖析工作,但這僅僅只是一部分,而其餘的部分則被遮在了幕後。記得我們在分析虛擬機器的時候,曾說這麼說過:

當Python啟動後,首先會進行 "執行時環境" 的初始化,而關於 "執行時環境" 的初始化是一個非常複雜的過程。並且 "執行時環境" 和 "執行環境" 是不同的, "執行時環境" 是一個全域性的概念,而 "執行環境" 是一個棧幀。關於"執行時環境"我們後面將用單獨的一章進行剖析,這裡就假設初始化動作已經完成,我們已經站在了Python虛擬機器的門檻外面,只需要輕輕推動一下第一張骨牌,整個執行過程就像多米諾骨牌一樣,一環扣一環地展開。

所以這次,我們將回到時間的起點,從Python的應用程式被執行開始,一步一步緊緊跟隨Python的軌跡,完整地展示Python在啟動之初的所有動作。當我們根據Python完成所有的初始化動作之後,也就能對Python執行引擎執行位元組碼指令時的整個執行環境瞭如執掌了。

執行緒環境初始化

我們知道執行緒是作業系統排程的最小單元,那麼Python中的執行緒又是怎麼樣的呢?

執行緒模型

我們之前介紹棧幀的時候說過,通過Python啟動一個執行緒,那麼底層會通過C來啟動一個執行緒,然後啟動作業系統的一個原生執行緒(OS執行緒)。所以Python中的執行緒實際上是對OS執行緒的一個封裝,因此Python中的執行緒是貨真價實的。

然後Python還提供了一個PyThreadState(執行緒狀態)物件,維護OS執行緒執行的狀態資訊,相當於是OS執行緒的一個抽象描述。雖然真正用來執行的執行緒及其狀態肯定是由作業系統進行維護的,但是Python虛擬機器在執行的時候總需要另外一些與執行緒相關的狀態和資訊,比如是否發生了異常等等,這些資訊顯然作業系統是沒有辦法提供的。而PyThreadState物件正是Python為OS執行緒準備的、在虛擬機器層面儲存其狀態資訊的物件,也就是執行緒狀態物件。而在Python中,當前活動的OS執行緒對應的PyThreadState物件可以通過PyThreadState_GET獲得,有了執行緒狀態物件之後,就可以設定一些額外資訊了。具體內容,我們後面會說。

當然除了執行緒狀態物件之外,還有程序狀態物件,我們來看看兩者在Python底層的定義是什麼?它們位於 Include/pystate.h 中。

typedef struct _is PyInterpreterState;
typedef struct _ts PyThreadState;

裡面的 PyInterpreterState 表示程序狀態物件, PyThreadState 表示執行緒狀態物件。但是我們看到它們都是typedef起得一個別名,而定義的結構體 struct  _is 位於 Include/cpython/pystate.h 中, struct  _ts 位於 Include/internal/pycore_pystate.h中。

執行緒狀態物件:

struct _ts {
    struct _ts *prev;  //多個執行緒狀態物件也像連結串列一樣串起來, 因為一個程序裡面是可以包含多個執行緒的, prev指向上一個執行緒狀態物件
    struct _ts *next;  //指向下一個執行緒狀態物件
    PyInterpreterState *interp;  //程序狀態物件, 標識對應的執行緒是屬於哪一個程序的

    struct _frame *frame; //棧幀物件, 模擬執行緒中函式的呼叫堆疊
    int recursion_depth;  //遞迴深度
    //.....
    //.....
    //....
    uint64_t id; //執行緒id
};

程序狀態物件:

struct _is {

    struct _is *next; //當前程序的下一個程序
    struct _ts *tstate_head; //程序環境中的執行緒狀態物件的集合, 我們說執行緒狀態物件會形成一個連結串列, 這裡就是連結串列的頭結點

    int64_t id; //執行緒id
 	//....
    PyObject *audit_hooks;
};

我們說 PyInterpreterState 物件是對程序的模擬, PyThreadState 是對執行緒的模擬。我們之前分析虛擬機器的時候說過其執行環境,如果再將執行時環境加進去的話。

執行緒環境的初始化

在Python啟動之後,初始化的動作是從 Py_NewInterpreter 函式開始的,然後這個函式呼叫了 new_interpreter 函式完成初始化,我們分析會先從 new_interpreter 函式開始,當然 Py_NewInterpreter 裡面也做了一些工作,具體的後面會說。

我們知道在Windows平臺上,當執行一個可執行檔案時,作業系統首先建立一個程序核心。同理在Python中亦是如此,會在 new_interpreter 中呼叫 PyInterpreterState_New 建立一個嶄新的 PyInterpreterState物件。該函式位於 Python/pystate.c 中。

PyInterpreterState *
PyInterpreterState_New(void)
{
    //申請程序狀態物件所需要的記憶體
    PyInterpreterState *interp = PyMem_RawMalloc(sizeof(PyInterpreterState));
    if (interp == NULL) {
        return NULL;
    }
	
    //設定屬性
    //......
    //......
    return interp;
}

關於程序狀態物件我們不做過多解釋,只需要知道Python直譯器在啟動時,會建立一個、或者多個 PyInterpreterState 物件,然後通過內部的next指標將多個 PyInterpreterState 串成一個連結串列結構。

在呼叫 PyInterpreterState_New 成功建立 PyInterpreterState之後,會再接再厲,呼叫 PyThreadState_New 建立一個全新的程序狀態物件,相關函式定義同樣位於 Python/pystate.c 中。

PyThreadState *
PyThreadState_New(PyInterpreterState *interp)
{
    //我們注意到這個函式接收一個PyInterpreterState
    //這些說明了執行緒是依賴於程序的,因為需要程序分配資源,而且這個函式又呼叫了new_threadstate
    //除了傳遞PyInterpreterState之外,還傳了一個1,想也不用想肯定是建立的執行緒數量
    //這裡建立1個,也就是主執行緒(main thread)
    return new_threadstate(interp, 1);
}


static PyThreadState *
new_threadstate(PyInterpreterState *interp, int init)
{	
    _PyRuntimeState *runtime = &_PyRuntime;
    //為執行緒狀態物件申請記憶體
    PyThreadState *tstate = (PyThreadState *)PyMem_RawMalloc(sizeof(PyThreadState));
    if (tstate == NULL) {
        return NULL;
    }
	//設定從執行緒中獲取函式呼叫棧的操作
    if (_PyThreadState_GetFrame == NULL) {
        _PyThreadState_GetFrame = threadstate_getframe;
    }
	
    //設定該執行緒所在的程序
    tstate->interp = interp;
	
    //下面就是設定內部的成員屬性
    tstate->frame = NULL;  //棧幀
    tstate->recursion_depth = 0; //遞迴深度
    tstate->id = ++interp->tstate_next_unique_id;//執行緒id
    //......
    //......
    //......
    tstate->prev = NULL; //上一個執行緒狀態物件
    tstate->next = interp->tstate_head;//當前執行緒狀態物件的next, 我們看到指向了執行緒狀態物件連結串列的頭結點, 說明是頭插法
    if (tstate->next)
        //因為每個執行緒狀態物件的prev指標都要指向它的上一個執行緒狀態物件, 如果是頭結點的話, 那麼prev就指向NULL
        //但由於新的執行緒狀態物件在插入之後顯然就變成了連結串列的頭結點, 因此還需要將插入之間的頭結點的prev指向新插入的執行緒狀態物件
        tstate->next->prev = tstate;
    //將tstate_head設定為新的執行緒狀態物件(連結串列的頭結點)
    interp->tstate_head = tstate;

    //返回執行緒狀態物件
    return tstate;
}

PyInterpreterState_New 相同,  PyThreadState_New 申請記憶體,建立 PyThreadState 物件,並且對其中每個成員進行初始化。而且其中的prev指標和next指標分別指向了上一個執行緒狀態物件和下一個執行緒狀態物件。而且也肯定會存在某一時刻,存在多個 PyThreadState 物件形成一個連結串列,那麼什麼時刻會發生這種情況呢?顯然用鼻子想也知道這是在Python啟動多執行緒(下一章分析)的時候。

此外我們看到Python在插入執行緒狀態物件的時候採用的是頭插法。

我們說Python設定了從執行緒中獲取函式呼叫棧的操作,所謂函式呼叫棧就是我們前面章節說的PyFrameObject物件連結串列。而且在原始碼中,我們看到了 PyThreadState 關聯了 PyInterpreterStatePyInterpreterState 也關聯了 PyInterpreterState 。到目前為止,僅有的兩個物件建立起了聯絡。對應到Windows,或者說作業系統,我們說程序和執行緒建立了聯絡

PyInterpreterStatePyThreadState 建立了聯絡之後,那麼就很容易在 PyInterpreterStatePyThreadState 之間穿梭。並且在Python執行時環境中,會有一個變數(先買個關子)一直維護著當前活動的執行緒,更準確的說是當前活動執行緒(OS執行緒)對應的 PyThreadState 物件。初始時,該變數為NULL。在Python啟動之後建立了第一個 PyThreadState 之後,會用該 PyThreadState 物件呼叫 PyThreadState_Swap 函式來設定這個變數,函式位於 Python/pystate.c 中。

PyThreadState *
PyThreadState_Swap(PyThreadState *newts)
{	
    //呼叫了_PyThreadState_Swap, 裡面傳入了兩個引數, 第一個我們後面說, 顯然從名字上看我們知道這是個GIL相關的
    //第二個引數就是建立的執行緒狀態物件
    return _PyThreadState_Swap(&_PyRuntime.gilstate, newts);
}


PyThreadState *
_PyThreadState_Swap(struct _gilstate_runtime_state *gilstate, PyThreadState *newts)
{	
    //這裡是獲取當前的執行緒狀態物件, 並且保證執行緒的安全性
    PyThreadState *oldts = _PyRuntimeGILState_GetThreadState(gilstate);
    //將GIL交給newts
    _PyRuntimeGILState_SetThreadState(gilstate, newts);
    //....
    return oldts;
}

//通過&(gilstate)->tstate_current獲取當前執行緒
#define _PyRuntimeGILState_GetThreadState(gilstate) \
    ((PyThreadState*)_Py_atomic_load_relaxed(&(gilstate)->tstate_current))
//將newts設定為當前執行緒, 可以理解為發生了執行緒的切換
#define _PyRuntimeGILState_SetThreadState(gilstate, value) \
    _Py_atomic_store_relaxed(&(gilstate)->tstate_current, \
                             (uintptr_t)(value))

然後我們看到這兩個巨集裡面出現了 _Py_atomic_load_relaxed_Py_atomic_store_relaxed&(gilstate)->tstate_current ,這些又是什麼呢?還有到底哪個變數在維護這當前的活動執行緒對應的狀態物件呢?其實那兩個巨集已經告訴你了。

//Include/internal/pycore_pystate.h
struct _gilstate_runtime_state {
    //...
    //巨集裡面出現的gilstate就是該結構體例項, tstate_current指的就是當前活動的OS執行緒對應的狀態物件
    //同時也是獲取到GIL的Python執行緒
    _Py_atomic_address tstate_current;
    //...
};


//Include/internal/pycore_atomic.h
#define _Py_atomic_load_relaxed(ATOMIC_VAL) \
    _Py_atomic_load_explicit((ATOMIC_VAL), _Py_memory_order_relaxed)

#define _Py_atomic_store_relaxed(ATOMIC_VAL, NEW_VAL) \
    _Py_atomic_store_explicit((ATOMIC_VAL), (NEW_VAL), _Py_memory_order_relaxed)

#define _Py_atomic_load_explicit(ATOMIC_VAL, ORDER) \
    atomic_load_explicit(&((ATOMIC_VAL)->_value), ORDER)

#define _Py_atomic_store_explicit(ATOMIC_VAL, NEW_VAL, ORDER) \
    atomic_store_explicit(&((ATOMIC_VAL)->_value), NEW_VAL, ORDER)
//_Py_atomic_load_relaxed用到了_Py_atomic_load_explicit, _Py_atomic_load_explicit用到了atomic_load_explicit
//_Py_atomic_store_relaxed用到了_Py_atomic_store_explicit, _Py_atomic_store_explicit用到了atomic_store_explicit

//而atomic_load_explicit和atomic_store_explicit是系統標頭檔案stdatomic.h中定義的api,這是在系統的api中修改的,所以說是執行緒安全的

介紹完中間部分的內容,那麼我們可以從頭開始分析Python執行時的初始化了,我們說它是在 new_interpreter 函式中呼叫 _PyRuntime_Initialize 函式時開始的,函式位於 Python/pylifecycle.c 中。

PyThreadState *
Py_NewInterpreter(void)
{	
    //執行緒狀態物件
    PyThreadState *tstate = NULL;
    //傳入執行緒物件, 呼叫new_interpreter
    PyStatus status = new_interpreter(&tstate);
    //異常檢測
    if (_PyStatus_EXCEPTION(status)) {
        Py_ExitStatusException(status);
    }
    //返回執行緒狀態物件, 顯然不會返回一個NULL, 這就說明在new_interpreter中執行緒狀態物件就已經被設定了
    return tstate;
}

另外裡面出現了一個 PyStatus, 表示程式執行的狀態, 會檢測是否發生了異常,該結構體定義在 Include/cpython/initconfig.h 中。

typedef struct {
    enum {
        _PyStatus_TYPE_OK=0,
        _PyStatus_TYPE_ERROR=1,
        _PyStatus_TYPE_EXIT=2
    } _type;
    const char *func;
    const char *err_msg;
    int exitcode;
} PyStatus;

然後我們的重點是 new_interpreter函式,我們程序狀態物件的建立就是在這個函式裡面發生的,該函式位於Python/pylifecycle.c中。

static PyStatus
new_interpreter(PyThreadState **tstate_p)
{
    PyStatus status; //狀態物件
	
    //執行時初始化, 如果出現異常直接返回
    status = _PyRuntime_Initialize();
    if (_PyStatus_EXCEPTION(status)) {
        return status;
    }
    //......
    // 建立一個程序狀態物件
    PyInterpreterState *interp = PyInterpreterState_New();
    //......
    //根據程序狀態物件建立一個執行緒狀態物件, 維護對應OS執行緒的狀態
    PyThreadState *tstate = PyThreadState_New(interp);
    //將GIL的控制權交給建立的執行緒
    PyThreadState *save_tstate = PyThreadState_Swap(tstate);

    //...
}

Python在初始化執行時環境時,肯定也要對型別系統進行初始化等等,整體是一個非常龐大的過程。有興趣的話,可以追根溯源對著原始碼閱讀以下。

到這裡,我們對 new_interpreter 算是有了一個階段性的成功,我們建立了代表程序和執行緒概念的 PyInterpreterStatePyThreadState 物件,並且在它們之間建立的聯絡。下面, new_interpreter 將進行入另一個環節,設定系統module。

建立__builtins__

在 new_interpreter 中當Python直譯器建立了 PyInterpreterStatePyThreadState 物件之後,就會開始設定系統的__builtins__了。

static PyStatus
new_interpreter(PyThreadState **tstate_p)
{
    //....
    //申請一個PyDictObject物件, 用於儲存所有的module物件
    //而我們說Python中的module物件都是存在sys.modules中的, 所以這裡的modules指的就是Python中的sys.modules
    PyObject *modules = PyDict_New();
    if (modules == NULL) {
        return _PyStatus_ERR("can't make modules dictionary");
    }
    
    //然後讓interp -> modules維護modules
    //我們翻看到這個interp表示的時程序例項物件, 這說明什麼? 顯然是該程序內的多個執行緒共享同一個內建名字空間
    interp->modules = modules;
	
    //載入sys模組, 我們說所有的module物件都在sys.modules中
    PyObject *sysmod = _PyImport_FindBuiltin("sys", modules);
    if (sysmod != NULL) {
        interp->sysdict = PyModule_GetDict(sysmod);
        if (interp->sysdict == NULL) {
            goto handle_error;
        }
        Py_INCREF(interp->sysdict);
        PyDict_SetItemString(interp->sysdict, "modules", modules);
        if (_PySys_InitMain(runtime, interp) < 0) {
            return _PyStatus_ERR("can't finish initializing sys");
        }
    }
	
    //載入內建模組, builtins是內建模組, 可以import builtins, 並且builtins.list等價於list
    PyObject *bimod = _PyImport_FindBuiltin("builtins", modules);
    if (bimod != NULL) {
        interp->builtins = PyModule_GetDict(bimod);
        if (interp->builtins == NULL)
            goto handle_error;
        Py_INCREF(interp->builtins);
    }
    //......
}

整體還是比較清晰和直觀的,另外我們說內建名字空間是由程序來維護的,因為程序就是用來為執行緒提供資源的。但是我們也能看出,這意味著一個程序內的多個執行緒共享同一個內建作用域,顯然這是非常合理的,不可能每開啟一個執行緒,就為其建立一個__builtins__。我們來從Python的角度證明這一點:

import threading
import builtins


def foo1():
    builtins.list, builtins.tuple = builtins.tuple, builtins.list


def foo2():
    print(f"猜猜下面程式碼會輸出什麼:")
    print("list:", list([1, 2, 3, 4, 5]))
    print("tuple:", tuple([1, 2, 3, 4, 5]))


f1 = threading.Thread(target=foo1)
f1.start()
f1.join()
threading.Thread(target=foo2).start()
"""
猜猜下面程式碼會輸出什麼:
list: (1, 2, 3, 4, 5)
tuple: [1, 2, 3, 4, 5]
"""

我們說所有的內建物件和內建函式都在內建名字空間裡面,我們可以通過 import builtins獲取、也可以直接通過__builtins__這個變數來獲取。我們在foo1中把list和tuple互換了,而這個結果顯然也影響到了foo2函式。這也說明了__builtins__是屬於程序級別的,它是被多個執行緒共享的。所以是interp -> modules = modules,當然這個modules是sys.modules,因為不止內建名字空間,所有的module物件都是被多個執行緒共享的。

而對__builts__的初始化時在 _PyBuiltin_Init 函式中進行的,它位於 Python/bltinmodule.c 中。

PyObject *
_PyBuiltin_Init(void)
{
    PyObject *mod, *dict, *debug;

    const PyConfig *config = &_PyInterpreterState_GET_UNSAFE()->config;

    if (PyType_Ready(&PyFilter_Type) < 0 ||
        PyType_Ready(&PyMap_Type) < 0 ||
        PyType_Ready(&PyZip_Type) < 0)
        return NULL;
	
    //建立並設定__builtins__ module
    mod = _PyModule_CreateInitialized(&builtinsmodule, PYTHON_API_VERSION);
    if (mod == NULL)
        return NULL;
    //將所有python內建物件加入到__builtins__ module中
    dict = PyModule_GetDict(mod);

    //......
    //老鐵們,下面這些東西應該不陌生吧   
    SETBUILTIN("None",                  Py_None);
    SETBUILTIN("Ellipsis",              Py_Ellipsis);
    SETBUILTIN("NotImplemented",        Py_NotImplemented);
    SETBUILTIN("False",                 Py_False);
    SETBUILTIN("True",                  Py_True);
    SETBUILTIN("bool",                  &PyBool_Type);
    SETBUILTIN("memoryview",        &PyMemoryView_Type);
    SETBUILTIN("bytearray",             &PyByteArray_Type);
    SETBUILTIN("bytes",                 &PyBytes_Type);
    SETBUILTIN("classmethod",           &PyClassMethod_Type);
    SETBUILTIN("complex",               &PyComplex_Type);
    SETBUILTIN("dict",                  &PyDict_Type);
    SETBUILTIN("enumerate",             &PyEnum_Type);
    SETBUILTIN("filter",                &PyFilter_Type);
    SETBUILTIN("float",                 &PyFloat_Type);
    SETBUILTIN("frozenset",             &PyFrozenSet_Type);
    SETBUILTIN("property",              &PyProperty_Type);
    SETBUILTIN("int",                   &PyLong_Type);
    SETBUILTIN("list",                  &PyList_Type);
    SETBUILTIN("map",                   &PyMap_Type);
    SETBUILTIN("object",                &PyBaseObject_Type);
    SETBUILTIN("range",                 &PyRange_Type);
    SETBUILTIN("reversed",              &PyReversed_Type);
    SETBUILTIN("set",                   &PySet_Type);
    SETBUILTIN("slice",                 &PySlice_Type);
    SETBUILTIN("staticmethod",          &PyStaticMethod_Type);
    SETBUILTIN("str",                   &PyUnicode_Type);
    SETBUILTIN("super",                 &PySuper_Type);
    SETBUILTIN("tuple",                 &PyTuple_Type);
    SETBUILTIN("type",                  &PyType_Type);
    SETBUILTIN("zip",                   &PyZip_Type);
    debug = PyBool_FromLong(config->optimization_level == 0);
    if (PyDict_SetItemString(dict, "__debug__", debug) < 0) {
        Py_DECREF(debug);
        return NULL;
    }
    Py_DECREF(debug);

    return mod;
#undef ADD_TO_ALL
#undef SETBUILTIN
}

整個 _PyBuiltin__Init 函式的功能就是設定好__builtins__ module,而這個過程是分為兩步的。

  • 通過_PyModule_CreateInitialized函式建立PyModuleObject物件,我們知道這是Python中模組物件的底層實現;
  • 設定module,將python中所有的內建物件都塞到__builtins__中

但是我們看到設定的東西似乎少了不少,比如dir、hasattr、setattr等等,這些明顯也是內建的,但是它們到哪裡去了。別急,我們剛才說建立__builtins__分為兩步,第一步是建立PyModuleObject,而使用的函式就是 _PyModule_CreateInitialized ,而在這個函式裡面就已經完成了大部分設定__builtins__的工作。該函式位於 Object/moduleobject.c 。

PyObject *
_PyModule_CreateInitialized(struct PyModuleDef* module, int module_api_version)
{
    const char* name;
    PyModuleObject *m;
	
    //初始化
    if (!PyModuleDef_Init(module))
        return NULL;
    //拿到module的name,對於當前來說,這裡顯然是__builtins__
    name = module->m_name;
    //這裡比較有意思,這是檢測模組版本的,針對的是需要匯入的py檔案。
    //我們說編譯成PyCodeObject物件之後,會直接從當前目錄的__pycache__裡面匯入
    //而那裡面都是pyc檔案,介紹位元組碼的時候我們說,pyc檔案的檔名是有Python直譯器的版本號的
    //這裡就是比較版本是否一致,不一致則不匯入pyc檔案,而是會重新編譯py檔案    
    if (!check_api_version(name, module_api_version)) {
        return NULL;
    }
    if (module->m_slots) {
        PyErr_Format(
            PyExc_SystemError,
            "module %s: PyModule_Create is incompatible with m_slots", name);
        return NULL;
    }
    //建立一個PyModuleObject 
    if ((m = (PyModuleObject*)PyModule_New(name)) == NULL)
        return NULL;

    //.......
    if (module->m_methods != NULL) {
        //遍歷methods中指定的module物件中應包含的操作集合
        if (PyModule_AddFunctions((PyObject *) m, module->m_methods) != 0) {
            Py_DECREF(m);
            return NULL;
        }
    }
    if (module->m_doc != NULL) {
        //設定docstring
        if (PyModule_SetDocString((PyObject *) m, module->m_doc) != 0) {
            Py_DECREF(m);
            return NULL;
        }
    }
    m->md_def = module;
    return (PyObject*)m;
}

根據上面的程式碼我們可以得出如下資訊:

  • 1. name:module物件的名稱,在這裡就是__builtins__
  • 2. module_api_version:python內部使用的version值,用於比較
  • 3. PyModule_New:用於建立一個PyModuleObject物件
  • 4. methods:該module中所包含的函式的集合,在這裡是builtin_methods
  • 5. PyModule_AddFunctions:設定methods中的函式操作
  • 6. PyModule_SetDocString:設定docstring

建立module物件

我們說Python中的module物件在底層cpython中對應的結構體是PyModuleObject物件,我們來看看它長什麼樣子吧,定義在 Objects/moduleobject.c 中。

typedef struct {
    PyObject_HEAD  //頭部資訊
    PyObject *md_dict;  //屬性字典, 所有的屬性和值都在裡面
    struct PyModuleDef *md_def;  //module物件包含的操作集合, 裡面是一些結構體, 每個結構體包含一個函式的相關資訊
    //...
    PyObject *md_name;  //模組名
} PyModuleObject;

而這個物件我們知道是通過PyModule_New建立的。

PyObject *
PyModule_New(const char *name)
{	
    //module物件的name、PyModuleObject
    PyObject *nameobj, *module;
    nameobj = PyUnicode_FromString(name);
    if (nameobj == NULL)
        return NULL;
    //根據建立PyModuleObject
    module = PyModule_NewObject(nameobj);
    Py_DECREF(nameobj);
    return module;
}


PyObject *
PyModule_NewObject(PyObject *name)
{	
    //建立一個module物件
    PyModuleObject *m;
    //申請空間
    m = PyObject_GC_New(PyModuleObject, &PyModule_Type);
    if (m == NULL)
        return NULL;
    //設定相應屬性, 初始化為NULL
    m->md_def = NULL;
    m->md_state = NULL;
    m->md_weaklist = NULL;
    m->md_name = NULL;
    //屬性字典
    m->md_dict = PyDict_New();
    //呼叫module_init_dict
    if (module_init_dict(m, m->md_dict, name, NULL) != 0)
        goto fail;
    PyObject_GC_Track(m);
    return (PyObject *)m;

 fail:
    Py_DECREF(m);
    return NULL;
}

static int
module_init_dict(PyModuleObject *mod, PyObject *md_dict,
                 PyObject *name, PyObject *doc)
{
    _Py_IDENTIFIER(__name__);
    _Py_IDENTIFIER(__doc__);
    _Py_IDENTIFIER(__package__);
    _Py_IDENTIFIER(__loader__);
    _Py_IDENTIFIER(__spec__);

    if (md_dict == NULL)
        return -1;
    if (doc == NULL)
        doc = Py_None;
	//模組的一些屬性、__name__、__doc__等等
    if (_PyDict_SetItemId(md_dict, &PyId___name__, name) != 0)
        return -1;
    if (_PyDict_SetItemId(md_dict, &PyId___doc__, doc) != 0)
        return -1;
    if (_PyDict_SetItemId(md_dict, &PyId___package__, Py_None) != 0)
        return -1;
    if (_PyDict_SetItemId(md_dict, &PyId___loader__, Py_None) != 0)
        return -1;
    if (_PyDict_SetItemId(md_dict, &PyId___spec__, Py_None) != 0)
        return -1;
    if (PyUnicode_CheckExact(name)) {
        Py_INCREF(name);
        Py_XSETREF(mod->md_name, name);
    }

    return 0;
}

這裡雖然建立了一個module物件,但是這僅僅是一個空的module物件,卻並沒有包含相應的操作和資料。我們看到只設置了name和doc等屬性。

設定module物件

在PyModule_New結束之後,程式繼續執行 _PyModule_CreateInitialized 下面的程式碼,然後我們知道通過 PyModule_AddFunctions 完成了對__builtins__幾乎全部屬性的設定。這個設定的屬性依賴於第二個引數methods,在這裡為builtin_methods。然後會遍歷builtin_methods,並處理每一項元素,我們還是來看看長什麼樣子。

//Python/bltinmodule.c

static PyMethodDef builtin_methods[] = {
    {"__build_class__", (PyCFunction)(void(*)(void))builtin___build_class__,
     METH_FASTCALL | METH_KEYWORDS, build_class_doc},
    {"__import__",      (PyCFunction)(void(*)(void))builtin___import__, METH_VARARGS | METH_KEYWORDS, import_doc},
    BUILTIN_ABS_METHODDEF
    BUILTIN_ALL_METHODDEF
    BUILTIN_ANY_METHODDEF
    BUILTIN_ASCII_METHODDEF
    BUILTIN_BIN_METHODDEF
    {"breakpoint",      (PyCFunction)(void(*)(void))builtin_breakpoint, METH_FASTCALL | METH_KEYWORDS, breakpoint_doc},
    BUILTIN_CALLABLE_METHODDEF
    BUILTIN_CHR_METHODDEF
    BUILTIN_COMPILE_METHODDEF
    BUILTIN_DELATTR_METHODDEF
    {"dir",             builtin_dir,        METH_VARARGS, dir_doc},
    BUILTIN_DIVMOD_METHODDEF
    BUILTIN_EVAL_METHODDEF
    BUILTIN_EXEC_METHODDEF
    BUILTIN_FORMAT_METHODDEF
    {"getattr",         (PyCFunction)(void(*)(void))builtin_getattr, METH_FASTCALL, getattr_doc},
    BUILTIN_GLOBALS_METHODDEF
    BUILTIN_HASATTR_METHODDEF
    BUILTIN_HASH_METHODDEF
    BUILTIN_HEX_METHODDEF
    BUILTIN_ID_METHODDEF
    BUILTIN_INPUT_METHODDEF
    BUILTIN_ISINSTANCE_METHODDEF
    BUILTIN_ISSUBCLASS_METHODDEF
    {"iter",            (PyCFunction)(void(*)(void))builtin_iter,       METH_FASTCALL, iter_doc},
    BUILTIN_LEN_METHODDEF
    BUILTIN_LOCALS_METHODDEF
    {"max",             (PyCFunction)(void(*)(void))builtin_max,        METH_VARARGS | METH_KEYWORDS, max_doc},
    {"min",             (PyCFunction)(void(*)(void))builtin_min,        METH_VARARGS | METH_KEYWORDS, min_doc},
    {"next",            (PyCFunction)(void(*)(void))builtin_next,       METH_FASTCALL, next_doc},
    BUILTIN_OCT_METHODDEF
    BUILTIN_ORD_METHODDEF
    BUILTIN_POW_METHODDEF
    {"print",           (PyCFunction)(void(*)(void))builtin_print,      METH_FASTCALL | METH_KEYWORDS, print_doc},
    BUILTIN_REPR_METHODDEF
    BUILTIN_ROUND_METHODDEF
    BUILTIN_SETATTR_METHODDEF
    BUILTIN_SORTED_METHODDEF
    BUILTIN_SUM_METHODDEF
    {"vars",            builtin_vars,       METH_VARARGS, vars_doc},
    {NULL,              NULL},
};

怎麼樣,是不是看到了玄機。

總結一下就是:在 Py_NewInterpreter 中呼叫 new_interpreter 函式,然後在 new_interpreter 這個函式裡面,通過 PyInterpreterState_New 建立 PyInterpreterState ,然後傳遞 PyInterpreterState 呼叫 PyThreadState_New 得到 PyThreadState 物件。

接著就是執行各種初始化動作,然後在 new_interpreter 中呼叫 _PyBuiltin_Init 設定內建屬性,在程式碼的最後會設定大量的內建屬性(函式、物件)。但是有幾個卻不在裡面,比如:dir、getattr等等。所以中間呼叫的 _PyModule_CreateInitialized 不僅僅是初始化一個module物件,還會在初始化之後將我們沒有看到的一些屬性設定進去,在 _PyModule_CreateInitialized 裡面,先是使用 PyModule_New 建立一個PyModuleObject,在裡面設定了name和doc等屬性之後,再通過 PyModule_AddFunctions 設定methods,在這裡面我們看到了dir、getattr等內建屬性。當這些屬性設定完之後,退回到 _PyBuiltin_Init 函式中,再設定剩餘的大量屬性。之後,__builtins__就完成了。

另外 builtin_methods 是一個 PyMethodDef 型別的陣列,裡面是一個個的 PyMethodDef 結構體,而這個結構體定義在 Include/methodobject.h 中。

struct PyMethodDef {
    /* 內建的函式或者方法名 */
    const char  *ml_name;   
    /* 實現對應邏輯的PyCFunctionObject, 後面會說它和PyFunctionObject之間的區別 */
    PyCFunction ml_meth;    
    
    /* 引數型別 
    #define METH_VARARGS  0x0001  擴充套件位置引數
    #define METH_KEYWORDS 0x0002  擴充套件關鍵字引數
    #define METH_NOARGS   0x0004  不需要引數
    #define METH_O        0x0008  需要一個引數
    #define METH_CLASS    0x0010  被classmethod裝飾
    #define METH_STATIC   0x0020  被staticmethod裝飾   
    */
    int         ml_flags;   
    
    //函式的__dic__
    const char  *ml_doc; 
};
typedef struct PyMethodDef PyMethodDef;

對於這裡面每一個 PyMethodDef ,_PyModule_CreateInitialized 都會基於它建立一個 PyCFunctionObject 物件, 這個物件Python對函式指標的包裝, 當然裡面好包含了其它資訊。

typedef struct {
    PyObject_HEAD  //頭部資訊
    PyMethodDef *m_ml;  //PyMethodDef
    PyObject    *m_self;  //self引數
    PyObject    *m_module;  //__module__屬性
    PyObject    *m_weakreflist;  //弱引用列表, 不討論
    vectorcallfunc vectorcall;
} PyCFunctionObject;

而 PyCFunctionObject 物件則是通過 PyCFunction_New 完成的,該函式位於 Objects/methodobject.c 中。

PyObject *
PyCFunction_New(PyMethodDef *ml, PyObject *self)
{
    return PyCFunction_NewEx(ml, self, NULL);
}


PyObject *
PyCFunction_NewEx(PyMethodDef *ml, PyObject *self, PyObject *module)
{
    vectorcallfunc vectorcall;
    //判斷引數型別
    switch (ml->ml_flags & (METH_VARARGS | METH_FASTCALL | METH_NOARGS | METH_O | METH_KEYWORDS))
    {
        case METH_VARARGS:
        case METH_VARARGS | METH_KEYWORDS:
            vectorcall = NULL;
            break;
        case METH_FASTCALL:
            vectorcall = cfunction_vectorcall_FASTCALL;
            break;
        case METH_FASTCALL | METH_KEYWORDS:
            vectorcall = cfunction_vectorcall_FASTCALL_KEYWORDS;
            break;
        case METH_NOARGS:
            vectorcall = cfunction_vectorcall_NOARGS;
            break;
        case METH_O:
            vectorcall = cfunction_vectorcall_O;
            break;
        default:
            PyErr_Format(PyExc_SystemError,
                         "%s() method: bad call flags", ml->ml_name);
            return NULL;
    }

    PyCFunctionObject *op;
    //我們看到這裡也採用了快取池的策略
    op = free_list;
    if (op != NULL) {
        free_list = (PyCFunctionObject *)(op->m_self);
        (void)PyObject_INIT(op, &PyCFunction_Type);
        numfree--;
    }
    else {
        //否則重新申請
        op = PyObject_GC_New(PyCFunctionObject, &PyCFunction_Type);
        if (op == NULL)
            return NULL;
    }
    //設定屬性
    op->m_weakreflist = NULL;
    op->m_ml = ml;
    Py_XINCREF(self);
    op->m_self = self;
    Py_XINCREF(module);
    op->m_module = module;
    op->vectorcall = vectorcall;
    _PyObject_GC_TRACK(op);
    return (PyObject *)op;
}

_PyBuiltin__Init 之後,Python會把PyModuleObject物件中維護的那個PyDictObject物件抽取出來,將其賦值給 interp -> builtins

//moduleobject.c
PyObject *
PyModule_GetDict(PyObject *m)
{
    PyObject *d;
    if (!PyModule_Check(m)) {
        PyErr_BadInternalCall();
        return NULL;
    }
    d = ((PyModuleObject *)m) -> md_dict;
    assert(d != NULL);
    return d;
}


static PyStatus
new_interpreter(PyThreadState **tstate_p)
{
    //......
    PyObject *bimod = _PyImport_FindBuiltin("builtins", modules);
    if (bimod != NULL) {
        //通過PyModule_GetDict獲取屬性字典, 賦值給builtins
        interp->builtins = PyModule_GetDict(bimod);
        if (interp->builtins == NULL)
            goto handle_error;
        Py_INCREF(interp->builtins);
    }
    else if (PyErr_Occurred()) {
        goto handle_error;
    }
    //......
}

以後Python在需要訪問__builtins__時,直接訪問 interp->builtins 就可以了,不需要再到 interp->modules 裡面去找了。因為對於內建函式、屬性的使用在Python中會比較頻繁,所以這種加速機制是很有效的。

建立sys module

Python在建立並設定了__builtins__之後,會照貓畫虎,用同樣的流程來設定sys module,並像設定 interp->builtins 一樣設定 interp->sysdict 。

//Python/pylifecycle.c
static PyStatus
new_interpreter(PyThreadState **tstate_p)
{
    //.......
    PyObject *sysmod = _PyImport_FindBuiltin("sys", modules);
    if (sysmod != NULL) {
        interp->sysdict = PyModule_GetDict(sysmod);
        if (interp->sysdict == NULL) {
            goto handle_error;
        }
        Py_INCREF(interp->sysdict);
        //設定
        PyDict_SetItemString(interp->sysdict, "modules", modules);
        if (_PySys_InitMain(runtime, interp) < 0) {
            return _PyStatus_ERR("can't finish initializing sys");
        }
    }
    //.......
}

Python在建立了sys module之後,會在此module中設定一個Python搜尋module時的預設路徑集合。

//Python/pylifecycle.c
static PyStatus
new_interpreter(PyThreadState **tstate_p)
{
    //.......
    status = add_main_module(interp);
    //.......
}    


static PyStatus
add_main_module(PyInterpreterState *interp)
{
    PyObject *m, *d, *loader, *ann_dict;
    //將__main__新增進sys.modules中
    m = PyImport_AddModule("__main__");
    if (m == NULL)
        return _PyStatus_ERR("can't create __main__ module");

    d = PyModule_GetDict(m);
    ann_dict = PyDict_New();
    if ((ann_dict == NULL) ||
        (PyDict_SetItemString(d, "__annotations__", ann_dict) < 0)) {
        return _PyStatus_ERR("Failed to initialize __main__.__annotations__");
    }
    Py_DECREF(ann_dict);

    if (PyDict_GetItemString(d, "__builtins__") == NULL) {
        PyObject *bimod = PyImport_ImportModule("builtins");
        if (bimod == NULL) {
            return _PyStatus_ERR("Failed to retrieve builtins module");
        }
        if (PyDict_SetItemString(d, "__builtins__", bimod) < 0) {
            return _PyStatus_ERR("Failed to initialize __main__.__builtins__");
        }
        Py_DECREF(bimod);
    }
    loader = PyDict_GetItemString(d, "__loader__");
    if (loader == NULL || loader == Py_None) {
        PyObject *loader = PyObject_GetAttrString(interp->importlib,
                                                  "BuiltinImporter");
        if (loader == NULL) {
            return _PyStatus_ERR("Failed to retrieve BuiltinImporter");
        }
        if (PyDict_SetItemString(d, "__loader__", loader) < 0) {
            return _PyStatus_ERR("Failed to initialize __main__.__loader__");
        }
        Py_DECREF(loader);
    }
    return _PyStatus_OK();
}

根據我們使用Python的經驗,我們知道最終Python肯定會建立一個PyListObject物件,也就是Python中的sys.path,裡面包含了一組PyUnicodeObject,每一個PyUnicodeObject的內容就代表了一個搜尋路徑。但是這一步不是在這裡完成的,至於是在哪裡完成的,我們後面會說。

另外,我們需要注意的是:在上面的邏輯中,直譯器將__main__這個模組新增進去了,這個__main__估計不用我多說了。之前在 PyModule_New 中,建立一個PyModuleObject物件之後,會在其屬性字典(md_dict獲取)中插入一個名為"__name__"的key,value就是 "__main__"。但是對於當然模組來說,這個模組也可以叫做__main__。

name = "神楽七奈"
import __main__
print(__main__.name)  # 神楽七奈

import sys
print(sys.modules["__main__"] is __main__)  # True

我們發現這樣也是可以匯入的,因為這個__main__就是這個模組本身。

static PyStatus
add_main_module(PyInterpreterState *interp)
{
    PyObject *m, *d, *loader, *ann_dict;
    //建立__main__ module,並將其插入到interp->modules中
    m = PyImport_AddModule("__main__");
    if (m == NULL)
        return _PyStatus_ERR("can't create __main__ module");
    //獲取__main__的屬性字典
    d = PyModule_GetDict(m);
    
    //獲取interp->modules中的__builtins__ module
    if (PyDict_GetItemString(d, "__builtins__") == NULL) {
        PyObject *bimod = PyImport_ImportModule("builtins");
        if (bimod == NULL) {
            return _PyStatus_ERR("Failed to retrieve builtins module");
        }
        //將("__builtins__", __builtins__)插入到__main__ module的dict中
        if (PyDict_SetItemString(d, "__builtins__", bimod) < 0) {
            return _PyStatus_ERR("Failed to initialize __main__.__builtins__");
        }
        Py_DECREF(bimod);
    }

    //......
}

因此我們算是知道了,為什麼python xxx.py執行的時候,__name__是__main__了,因為我們這裡設定了。而Python沿著名字空間尋找的時候,最終會在__main__的local空間中發現__name__,且值為字串"__main__"。但如果是以import的方式載入的,那麼__name__則不是"__main__",而是模組名,後面我們會繼續說。

其實這個__main__我們是再熟悉不過的了,當輸入dir()的時候,就會顯示__main__的內容。dir是可以不加引數的,如果不加引數,那麼預設訪問當前的py檔案,也就是__main__。

>>> __name__
'__main__'
>>>
>>> __builtins__.__name__
'builtins'
>>>
>>> import numpy as np
>>> np.__name__
'numpy'
>>>

所以說,訪問模組就類似訪問變數一樣。modules裡面存放了所有的(module name, PyModuleObject),當我們呼叫np的時候,是會找到name為"numpy"的值,然後這個值裡面也維護了一個字典,其中就有一個key為__name__的entry。

設定site-specific的module的搜尋路徑

Python是一個非常開放的體系,它的強大來源於豐富的第三方庫,這些庫由外部的py檔案來提供,當使用這些第三方庫的時候,只需要簡單的進行import即可。一般來說,這些第三方庫都放在/lib/site-packages中,如果程式想使用這些庫,直接把庫放在這裡面即可。

但是到目前為止,我們好像也沒看到python將site-packages路徑設定到搜尋路徑裡面去啊。其實在完成了__main__的建立之後,Python才騰出手來,收拾這個site-package。這個關鍵的動作在於Python的一個標準庫:site.py。

我們先來將Lib目錄下的site.py刪掉,然後匯入一個第三方模組,看看會有什麼後果。

因此我們發現,Python在初始化的過程中確實匯入了site.py,所以才有瞭如下的輸出。而這個site.py也正是Python能正確載入位於site-packages目錄下第三方包的關鍵所在。我們可以猜測,應該就是這個site.py將site-packages目錄加入到了前面的sys.path中,而這個動作是由 init_import_size 完成的。

static PyStatus
new_interpreter(PyThreadState **tstate_p)
{
    //......
        if (config->site_import) {
            status = init_import_size();
            if (_PyStatus_EXCEPTION(status)) {
                return status;
            }
        }
    //......
}


static PyStatus
init_import_size(void)
{
    PyObject *m;
    m = PyImport_ImportModule("site");
    if (m == NULL) {
        //這裡的報錯資訊是不是和上圖中顯示的一樣呢?
        return _PyStatus_ERR("Failed to import the site module");
    }
    Py_DECREF(m);
    return _PyStatus_OK();
}

init_import_size 中,只調用了 PyImport_ImportModule 函式,這個函式是Python中import機制的核心所在。PyImport_ImportModule("numpy")等價於python中的 import numpy 即可。

啟用python虛擬機器

Python執行方式有兩種,一種是在命令列中執行的互動式環境;另一種則是以python xxx.py方式執行指令碼檔案。儘管方式不同,但是卻殊途同歸,進入同一個位元組碼虛擬機器。

Python在 Py_Initialize 完成之後,最終會通過 pymain_run_file 呼叫 PyRun_AnyFileExFlags

//Modules/main.c
static int
pymain_run_file(PyConfig *config, PyCompilerFlags *cf)
{	
    //那麼獲取檔名
    const wchar_t *filename = config->run_filename;
    if (PySys_Audit("cpython.run_file", "u", filename) < 0) {
        return pymain_exit_err_print();
    }
    //開啟檔案
    FILE *fp = _Py_wfopen(filename, L"rb");
    //如果fp為NULL, 證明檔案開啟失敗
    if (fp == NULL) {
        char *cfilename_buffer;
        const char *cfilename;
        int err = errno;
        cfilename_buffer = _Py_EncodeLocaleRaw(filename, NULL);
        if (cfilename_buffer != NULL)
            cfilename = cfilename_buffer;
        else
            cfilename = "<unprintable file name>";
        fprintf(stderr, "%ls: can't open file '%s': [Errno %d] %s\n",
                config->program_name, cfilename, err, strerror(err));
        PyMem_RawFree(cfilename_buffer);
        return 2;
    }
    //......
    //呼叫PyRun_AnyFileExFlags
    int run = PyRun_AnyFileExFlags(fp, filename_str, 1, cf);
    Py_XDECREF(bytes);
    return (run != 0);
}


//Python/pythonrun.c
int
PyRun_AnyFileExFlags(FILE *fp, const char *filename, int closeit,
                     PyCompilerFlags *flags)
{
    if (filename == NULL)
        filename = "???";
    //根據fp是否代表互動環境,對程式進行流程控制
    if (Py_FdIsInteractive(fp, filename)) {
        //如果是互動環境,那麼呼叫PyRun_InteractiveLoopFlags
        int err = PyRun_InteractiveLoopFlags(fp, filename, flags);
        if (closeit)
            fclose(fp);
        return err;
    }
    else
        //否則說明是一個普通的python指令碼,執行PyRun_SimpleFileExFlags
        return PyRun_SimpleFileExFlags(fp, filename, closeit, flags);
}

我們看到互動式和py指令碼式走的兩條不同的路徑,但是彆著急,最終你會看到它們又會分久必合、走向同一條路徑。

互動式執行

先來看看互動式執行時候的情形,不過在此之前先來看一下提示符。

>>> a = 1
>>> if a == 1:
...     pass
...
>>>
>>> import sys
>>> sys.ps1 = "matsuri:"
matsuri:a = 1
matsuri:a
1
matsuri:
matsuri:sys.ps2 = "fubuki:"
matsuri:if a == 1:
fubuki:    pass
fubuki:
matsuri:

我們每輸入一行,開頭都是>>> ,這個是sys.ps1,而輸入語句塊的時候,沒輸入完的時候,那麼顯示...,這個是sys.ps2。如果修改了,那麼就是我們自己定義的了。

int
PyRun_InteractiveLoopFlags(FILE *fp, const char *filename_str, PyCompilerFlags *flags)
{
    //....
    //建立互動式提示符 
    v = _PySys_GetObjectId(&PyId_ps1);
    if (v == NULL) {
        _PySys_SetObjectId(&PyId_ps1, v = PyUnicode_FromString(">>> "));
        Py_XDECREF(v);
    }
    //同理這個也是一樣
    v = _PySys_GetObjectId(&PyId_ps2);
    if (v == NULL) {
        _PySys_SetObjectId(&PyId_ps2, v = PyUnicode_FromString("... "));
        Py_XDECREF(v);
    }
    err = 0;
    do {
        //這裡就進入了互動式環境,我們看到每次都呼叫了PyRun_InteractiveOneObjectEx
        //直到下面的ret != E_EOF不成立 停止迴圈,一般情況就是我們輸入exit()圖此處了
        ret = PyRun_InteractiveOneObjectEx(fp, filename, flags);
        if (ret == -1 && PyErr_Occurred()) {
            if (PyErr_ExceptionMatches(PyExc_MemoryError)) {
                if (++nomem_count > 16) {
                    PyErr_Clear();
                    err = -1;
                    break;
                }
            } else {
                nomem_count = 0;
            }
            PyErr_Print();
            flush_io();
        } else {
            nomem_count = 0;
        }
    //......
    } while (ret != E_EOF);
    Py_DECREF(filename);
    return err;
}


static int
PyRun_InteractiveOneObjectEx(FILE *fp, PyObject *filename,
                             PyCompilerFlags *flags)
{
    PyObject *m, *d, *v, *w, *oenc = NULL, *mod_name;
    mod_ty mod;
    PyArena *arena;
    const char *ps1 = "", *ps2 = "", *enc = NULL;
    int errcode = 0;
    _Py_IDENTIFIER(encoding);
    _Py_IDENTIFIER(__main__);

    mod_name = _PyUnicode_FromId(&PyId___main__); /* borrowed */
    if (mod_name == NULL) {
        return -1;
    }

    if (fp == stdin) {
        //......
    }
    v = _PySys_GetObjectId(&PyId_ps1);
    if (v != NULL) {
        //......
    }
    w = _PySys_GetObjectId(&PyId_ps2);
    if (w != NULL) {
        //.....
    }
    //編譯使用者在互動式環境下輸入的python語句
    arena = PyArena_New();
    if (arena == NULL) {
        Py_XDECREF(v);
        Py_XDECREF(w);
        Py_XDECREF(oenc);
        return -1;
    }
    //生成抽象語法樹
    mod = PyParser_ASTFromFileObject(fp, filename, enc,
                                     Py_single_input, ps1, ps2,
                                     flags, &errcode, arena);
    Py_XDECREF(v);
    Py_XDECREF(w);
    Py_XDECREF(oenc);
    if (mod == NULL) {
        PyArena_Free(arena);
        if (errcode == E_EOF) {
            PyErr_Clear();
            return E_EOF;
        }
        return -1;
    }
    //獲取<module __main__>中維護的dict
    m = PyImport_AddModuleObject(mod_name);
    if (m == NULL) {
        PyArena_Free(arena);
        return -1;
    }
    d = PyModule_GetDict(m);
    //執行使用者輸入的python語句
    v = run_mod(mod, filename, d, d, flags, arena);
    PyArena_Free(arena);
    if (v == NULL) {
        return -1;
    }
    Py_DECREF(v);
    flush_io();
    return 0;
}

我們發現在run_mod之前,python會將__main__中維護的PyDictObject物件取出,作為引數傳遞給run_mod,這個引數關係極為重要,實際上這裡的引數d就將作為Python虛擬機器開始執行時當前活動的frame物件的local名字空間和global名字空間。

指令碼檔案執行方式

接下來,我們看一看直接執行指令碼檔案的方式。

//.include/compile.h
#define Py_file_input 257


//Python/pythonrun.c
int
PyRun_SimpleFileExFlags(FILE *fp, const char *filename, int closeit,
                        PyCompilerFlags *flags)
{
    PyObject *m, *d, *v;
    const char *ext;
    int set_file_name = 0, ret = -1;
    size_t len;
    //__main__就是當前檔案
    m = PyImport_AddModule("__main__");
    if (m == NULL)
        return -1;
    Py_INCREF(m);
    //還記得這個d嗎?當前活動的frame物件的local和global名字空間
    d = PyModule_GetDict(m);
    //在__main__中設定__file__屬性
    if (PyDict_GetItemString(d, "__file__") == NULL) {
        PyObject *f;
        f = PyUnicode_DecodeFSDefault(filename);
        if (f == NULL)
            goto done;
        if (PyDict_SetItemString(d, "__file__", f) < 0) {
            Py_DECREF(f);
            goto done;
        }
        if (PyDict_SetItemString(d, "__cached__", Py_None) < 0) {
            Py_DECREF(f);
            goto done;
        }
        set_file_name = 1;
        Py_DECREF(f);
    }
    len = strlen(filename);
    ext = filename + len - (len > 4 ? 4 : 0);
    //如果是pyc
    if (maybe_pyc_file(fp, filename, ext, closeit)) {
        FILE *pyc_fp;
        //二進位制模式開啟
        if (closeit)
            fclose(fp);
        if ((pyc_fp = _Py_fopen(filename, "rb")) == NULL) {
            fprintf(stderr, "python: Can't reopen .pyc file\n");
            goto done;
        }

        if (set_main_loader(d, filename, "SourcelessFileLoader") < 0) {
            fprintf(stderr, "python: failed to set __main__.__loader__\n");
            ret = -1;
            fclose(pyc_fp);
            goto done;
        }
        v = run_pyc_file(pyc_fp, filename, d, d, flags);
    } else {
        if (strcmp(filename, "<stdin>") != 0 &&
            set_main_loader(d, filename, "SourceFileLoader") < 0) {
            fprintf(stderr, "python: failed to set __main__.__loader__\n");
            ret = -1;
            goto done;
        }
        //執行指令碼檔案
        v = PyRun_FileExFlags(fp, filename, Py_file_input, d, d,
                              closeit, flags);
    }
    //.......
}


PyObject *
PyRun_FileExFlags(FILE *fp, const char *filename_str, int start, PyObject *globals,
                  PyObject *locals, int closeit, PyCompilerFlags *flags)
{
    PyObject *ret = NULL;
    //......
    //編譯
    mod = PyParser_ASTFromFileObject(fp, filename, NULL, start, 0, 0,
                                     flags, NULL, arena);
    if (closeit)
        fclose(fp);
    if (mod == NULL) {
        goto exit;
    }
    //執行, 依舊是呼叫了runmod
    ret = run_mod(mod, filename, globals, locals, flags, arena);

exit:
    Py_XDECREF(filename);
    if (arena != NULL)
        PyArena_Free(arena);
    return ret;
}

很顯然,指令碼檔案和互動式之間的執行流程是不同的,但是最終都進入了run_mod,而且同樣也將與__main__中維護的PyDictObject物件作為local名字空間和global名字空間傳入了run_mod。

啟動虛擬機器

是的你沒有看錯,下面才是啟動虛擬機器,之前做了那麼工作都是前戲。

static PyObject *
run_mod(mod_ty mod, PyObject *filename, PyObject *globals, PyObject *locals,
            PyCompilerFlags *flags, PyArena *arena)
{
    PyCodeObject *co;
    PyObject *v;
    //基於ast編譯位元組碼指令序列,建立PyCodeObject物件
    co = PyAST_CompileObject(mod, filename, flags, -1, arena);
    if (co == NULL)
        return NULL;

    if (PySys_Audit("exec", "O", co) < 0) {
        Py_DECREF(co);
        return NULL;
    }
	
    //建立PyFrameObject,執行PyCodeObject物件中的位元組碼指令序列
    v = run_eval_code_obj(co, globals, locals);
    Py_DECREF(co);
    return v;
}

run_mod接手傳來的ast,然後傳到 PyAST_CompileObject 中,建立了一個我們已經非常熟悉的PyCodeObject物件。關於這個完整的編譯過程,就又是另一個話題了,總之先是scanner進行詞法分析、將原始碼切分成一個個的token,然後parser在詞法分析之後的結果之上進行語法分析、通過切分好的token生成抽象語法樹(AST,abstract syntax tree),然後將AST編譯PyCodeObject物件,最後再由虛擬機器執行。知道這麼一個大致的流程即可,至於到底是怎麼分詞、怎麼建立語法樹的,這就又是一個難點了,個人覺得甚至比研究Python虛擬機器還難。有興趣的話可以去看Python原始碼中Parser目錄,如果能把Python的分詞、語法樹的建立給瞭解清楚,那我覺得你完全可以手寫一個正則表示式的引擎、以及各種模板語言。

而接下來,Python已經做好一切工作,開始通過 run_eval_code_obj 著手喚醒位元組碼虛擬機器。

static PyObject *
run_eval_code_obj(PyCodeObject *co, PyObject *globals, PyObject *locals)
{
    PyObject *v;
    //......
    v = PyEval_EvalCode((PyObject*)co, globals, locals);
    if (!v && PyErr_Occurred() == PyExc_KeyboardInterrupt) {
        _Py_UnhandledKeyboardInterrupt = 1;
    }
    return v;
}

函式中呼叫了 PyEval_EvalCode,根據前面介紹函式的時候,我們知道最終一定會走到 PyEval_EvalFrameEx

從作業系統建立程序,程序建立執行緒,執行緒設定builtins(包括設定__name__、內建物件、內建函式方法等等)、設定快取池,然後各種初始化,設定搜尋路徑。最後分詞、編譯、啟用虛擬機器執行。而執行的這個過程就是曾經與我們朝夕相處的 PyEval_EvalFrameEx ,掌控Python世界中無數物件的生生滅滅。引數f就是PyFrameObject物件,我們曾經探索了很久,現在一下子就回到了當初。有種夢迴棧幀物件的感覺。目前的話,Python的骨架我們已經看清了,雖然還有很多細節隱藏在幕後。至少神祕的面紗已經被撤掉了。

名字空間

現在我們來看一下有趣的東西,看看在啟用位元組碼虛擬機器、建立 PyFrameObject 物件時,所設定的3個名字空間:local、global、builtin。

//Objects/frameobject.c
PyFrameObject*
PyFrame_New(PyThreadState *tstate, PyCodeObject *code,
            PyObject *globals, PyObject *locals)
{
    PyFrameObject *f = _PyFrame_New_NoTrack(tstate, code, globals, locals);
    if (f)
        _PyObject_GC_TRACK(f);
    return f;
}


PyFrameObject* _Py_HOT_FUNCTION
_PyFrame_New_NoTrack(PyThreadState *tstate, PyCodeObject *code,
                     PyObject *globals, PyObject *locals)
{
    PyFrameObject *back = tstate->frame;
    PyFrameObject *f;
    PyObject *builtins;
    Py_ssize_t i;

    //設定builtin名字空間
    if (back == NULL || back->f_globals != globals) {
        //但是我們發現設定builtins,居然是從globals裡面獲取的
        //帶著這個疑問,看看下面更大的疑問        
        builtins = _PyDict_GetItemIdWithError(globals, &PyId___builtins__);
        //......
    }
    else {
        /* If we share the globals, we share the builtins.
           Save a lookup and a call. */
        builtins = back->f_builtins;
        assert(builtins != NULL);
        Py_INCREF(builtins);
    }
    //.......
    //設定builtins
    f->f_builtins = builtins;
    //....
    //設定globals
    f->f_globals = globals;

    if ((code->co_flags & (CO_NEWLOCALS | CO_OPTIMIZED)) ==
        (CO_NEWLOCALS | CO_OPTIMIZED))
        ; /* f_locals = NULL; will be set by PyFrame_FastToLocals() */
    else if (code->co_flags & CO_NEWLOCALS) {
        locals = PyDict_New();
        if (locals == NULL) {
            Py_DECREF(f);
            return NULL;
        }
        f->f_locals = locals;
    }
    else {
        if (locals == NULL)
            //如果locals為NULL,那麼等同於globals,顯然這是針對模組來的
            locals = globals;
        Py_INCREF(locals);
        f->f_locals = locals;
    }

    f->f_lasti = -1;
    f->f_lineno = code->co_firstlineno;
    f->f_iblock = 0;
    f->f_executing = 0;
    f->f_gen = NULL;
    f->f_trace_opcodes = 0;
    f->f_trace_lines = 1;

    return f;
}

我們說內建名字空間是從global名字空間裡面獲取的,我們用Python來演示一下。

# 代表了globals()裡面存放了builtins
print(globals()["__builtins__"])  # <module 'builtins' (built-in)>

# 我們說builtins裡面包含了所有的內建物件、函式等等,顯然呼叫int是可以的
print(globals()["__builtins__"].int("123"))  # 123

# 但是,我居然能從builtins裡面拿到globals
# 不過也很好理解,因為globals是一個內建函式,肯定是在builtins裡面
print(globals()["__builtins__"].globals)  # <built-in function globals>

# 於是拿到了globals,繼續呼叫,然後獲取__builtins__,又拿到了builtins,而且我們是可以呼叫list的
print(globals()["__builtins__"].globals()["__builtins__"].list("abcd"))  # ['a', 'b', 'c', 'd']

所以不管套娃多少次,都是可以的,因為它們都是指標。

可以看到builtin和global空間裡面都儲存一個能夠獲取對方空間的一個函式指標, 所以這兩者是並不衝突的。當然除此之外,還有一個__name__,注意我們之前說設定__name__只是builtins的__name__,並不是當前模組的。

# 我們看到,builtins裡面獲取的__name__居然不是__main__,而是builtins
print(globals()["__builtins__"].__name__)  # builtins

# 首先按照local  global builtin的順序查詢是沒問題的
# 而對於模組來說,我們知道local空間為NULL的話,然後直接把global空間交給local空間了
# 而local裡面有__name__,就是__main__,所以__name__和builtins.__name__不是一個東西
print(globals()["__name__"])  # __main__
# 初始化builtins的時候,那個__name__指的是builtins這個PyModuleObject的__name__
# 而對於我們py檔案這個模組來說,__name__是設定在global名字空間裡面的

# 如果將global空間或者local空間裡面的__name__刪掉,那麼按照順序就會尋找builtin裡面的__name__,此時就會列印builtins了。
globals().pop("__name__")
print(__name__)  # builtins

所以我們看到__name__這個屬性是在啟動之後動態設定的,如果執行的檔案和該檔案是同一個檔案,那麼__name__就會是__main__;如果不是同一個檔案,證明這個檔案是作為模組被匯入進來的,那麼此時它的__name__就是檔名。

更多細節可以前往原始碼中檢視,Python執行環境初始化還是比較複雜的。

小結

這一次我們說了Python執行環境的初始化,或者說當Python啟動的時候都做了哪些事情。可以看到,做的事情不是一般的多,真的準備了大量的工作。因為Python是動態語言,這就意味很多操作都要發生在執行時。