Python 源碼分析-運行機制

說明

  • python源碼版本:3.8.3

參考:《python源碼剖析》

python運行機制

當python代碼運行時,會將代碼轉成一堆的字節指令,然後通過PyEval_EvalFrame函數執行裏面的內容,源碼如下:

// ~/Python/ceval.c

// python代碼執行的入口函數
PyObject *
PyEval_EvalFrame(PyFrameObject *f) {
    // 傳入一個棧幀對象,並傳入PyEval_EvalFrameEx函數執行
    return PyEval_EvalFrameEx(f, 0);
}

PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
    // 獲取解釋器對象,可以理解爲當前執行的進程
    PyInterpreterState *interp = _PyInterpreterState_GET_UNSAFE();
    // 執行執行棧幀,eval_frame實際上就是_PyEval_EvalFrameDefault,定義如下:
    // interp->eval_frame = _PyEval_EvalFrameDefault;
    return interp->eval_frame(f, throwflag);
}

_PyEval_EvalFrameDefault函數主要是由for循環和一個巨大的switch語句組成的,其中switch語句裏定義了對各種opcode執行的操作,因此通過for循環不斷地從switch中尋找對應的指令操作執行,就組成了python程序(實際上是模擬了一個執行的函數棧對象),部分源碼如下:

// ~/Python/ceval.c

PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
{
    ...
    // 主循環
    main_loop:
        for (;;) {
            ...
            // 根據opcode執行對應的操作
            dispatch_opcode:
                ...
                switch (opcode) {
                    // NOP指令行爲
                    case TARGET(NOP): {
                        FAST_DISPATCH();
                    }
                    // LOAD_FAST指令行爲
                    case TARGET(LOAD_FAST): {
                        ...
                    }
                    // LOAD_CONST指令行爲
                    case TARGET(LOAD_CONST): {
                        ...
                    }
                    ...
                }
}

code對象

python中code對象記錄着代碼的運行環境、運行指令等,是python程序運行的基石,其主要屬性如下:

co_consts   常量表
co_varnames 變量名錶
co_names    命名空間表
co_nlocals  局部變量的數量
co_filename  code所在文件
co_name  code對應名稱(函數名、模塊名、類名)
co_argcount  位置參數數量(不包括擴展位置參數和擴展鍵參數,這兩個參數實際上是作爲局部變量存在local區裏)
co_kwonlyargcount 鍵參數數量
co_code  代碼的字節碼指令(包括opcode和oparg)
co_firstlineno  code代碼第一行在文件中的行數
co_cellvars  在外層函數裏記錄的嵌套作用域會使用到的變量名集合
co_freevars  在嵌套函數裏記錄使用到的外層作用域的變量名集合
co_stacksize    棧的大小
co_lnotab   字節碼指令和行號的對應關係
co_flags    對象的信息(例如記錄了是函數還是生成器?是否有擴展參數等)
co_flags

每一位標誌定義如下:

# 可以在inspect模塊下查看
1=optimized
2=newlocals
4=*arg 
8=**arg
16=nested
32=generator
64=nofree
128=coroutine
256=iterable_coroutine
512=async_generator
co_cellvars/co_freevars

co_freevars是每個閉包函數中記錄當前閉包函數裏會使用到的外部變量,而co_cellvars則是當前函數中記錄所有嵌套函數內部會使用到當前函數的變量,如果只有一個閉包函數使用到了外部函數的變量時,co_cellvarsco_freevars的內容可能一樣,舉例:

def test(a, b=2, c=3):
    d = [1,2,3,4,5]
    e = {}
    def cell_test1():
        d = (a, c)
    def cell_test2():
        f = e
    print("test:", test.__code__.co_cellvars)
    print("cell_test1:", cell_test1.__code__.co_freevars)
    print("cell_test2:", cell_test2.__code__.co_freevars)

test(1)

# test: ('a', 'c', 'e')
# cell_test1: ('a', 'c')
# cell_test2: ('e',)

可以看出cell_test1裏使用了accell_test2裏使用了e,而test裏則記錄了所有被使用到的變量

opcode

根據前面的介紹可以知道python程序的運行實際上就是不斷地讀取code對象中記錄的opcode以及對應的oparg來執行相應的動作,例如下面一段代碼:

def test(a, b, c):
    pass

def run():
    # a並沒有定義,但只要沒調用,也就不會檢查a是否存在
    a()
    test(1, 2, 3)

from dis import dis
dis(run)

可以看出run函數生成對應的opcode如下:

# 四列依次爲:指令偏移;opcode對應指令;oparg;oparg對應的值
 0 LOAD_GLOBAL              0 (a)  # 載入全局區的a
 2 CALL_FUNCTION            0      # 對a進行函數調用,並且由於沒有參數,所以傳入參數0
 4 POP_TOP                         # 彈出棧頂元素(CALL_FUNCTION執行完會將返回值壓入棧頂)
 6 LOAD_GLOBAL              1 (test)  # 載入test
 8 LOAD_CONST               1 (1)  # 依次將3個參數壓入棧
10 LOAD_CONST               2 (2)
12 LOAD_CONST               3 (3)
14 CALL_FUNCTION            3    # 函數調用,並從棧中依次取出3個元素
16 POP_TOP
18 LOAD_CONST               0 (None)
20 RETURN_VALUE

並且可以看出run函數中的a函數並沒有定義,但是並不會報錯,這是因爲python在對run函數解析對應的code對象時,只會對語法進行解析,並生成對應的opcodeoparg組成的字節流,而不會去管解析的語句是否能夠成功執行,所以只有在真正執行的時候,纔會知道是否存在問題

注:
爲了便於解析,python3.6開始規定每一組指令單位都是2個字節(python3.6之前是不固定的),通過前面示例解析的opcode(第一列)也可以看出每條指令偏移的結果是公差爲2的等差數列,其中opcodeoparg分別佔一個字節,源碼如下:

// 獲取當前指令的opcode和oparg,並指向下一條指令位置
#define NEXTOPARG()  do { \
        // 獲取opcode+oparg組成的指令(uint_16_t類型,總共2個字節)
        _Py_CODEUNIT word = *next_instr; \
        // 獲取opcode
        opcode = _Py_OPCODE(word); \
        // 獲取oparg
        oparg = _Py_OPARG(word); \
        // 指向下一條指令
        next_instr++; \
    } while (0)

// 獲取opcode、oparg邏輯,根據大/小端模式,取值方式有所不同
#ifdef WORDS_BIGENDIAN
#  define _Py_OPCODE(word) ((word) >> 8)
#  define _Py_OPARG(word) ((word) & 255)
#else
#  define _Py_OPCODE(word) ((word) & 255)
#  define _Py_OPARG(word) ((word) >> 8)
#endif

每個opcode都有對應的編號,其中編號大於90的屬於有參數指令,對於無參數的指令,默認會以0作爲參數,從而保證oparg部分佔有1個字節,源碼如下:

#define HAVE_ARGUMENT            90
// 判斷指令是否存在參數
#define HAS_ARG(op) ((op) >= HAVE_ARGUMENT)
常見opcode介紹
LOAD_FAST  載入局部變量,從棧幀對象的棧空間(f_localsplus)中取出數據
LOAD_CONST  載入常量表內容(code對象的co_consts當中)
LOAD_NAME  載入命名空間變量(code對象的co_names當中)
STORE_NAME  定義命名空間變量
STORE_FAST  定義局部變量
POP_TOP  彈出棧頂元素
RETURN_VALUE  返回棧頂元素
BUILD_LIST  創建一個列表,傳入一個參數代表列表內容長度
BUILD_MAP  創建一個字典
LOAD_ATTR  載入屬性
MAKE_FUNCTION  創建函數
CALL_FUNCTION  調用函數
...

這些opcode都會在python那個巨大的switch當中有對應的操作,例如CALL_FUNCTION的源碼操作如下:

// 調用函數
case TARGET(CALL_FUNCTION): {
    PREDICTED(CALL_FUNCTION);
    PyObject **sp, *res;
    // 記錄當前函數指針的位置
    sp = stack_pointer;
    // 調用函數,並將當前棧頂指針位置傳入
    res = call_function(tstate, &sp, oparg, NULL);
    // 函數執行完畢,將棧頂指針恢復到上一層函數的位置
    stack_pointer = sp;
    // 將結果返回值壓入棧頂
    PUSH(res);
    if (res == NULL) {
        goto error;
    }
    // 繼續下一條指令
    DISPATCH();
}

可以看出函數在執行完畢以後,會執行一次PUSH(res)操作,即把函數的返回值壓入棧頂,這也是Python的函數爲什麼必然有返回值的原因,如果不寫return語句,編譯器也會自動在函數的最後加上將None壓入棧的指令。而函數執行結束以後,因爲只會進行一次壓棧操作,所以不支持多個返回值,在這種情況下,我們一般通過返回一個序列對象來實現返回多個值的目的。

opcode作用

opcode是我們參考程序執行效率的重要指標之一,例如我們通過opcode對創建一定長度列表的代碼進行優化:

import time

def test():
    l = 10000000
    start = time.time()
    li = []
    for i in range(l):
        li.append(i)
    print(time.time() - start)

test()
# 1.726109504699707

可以發現程序的平均執行時間爲1.7s左右(不同電腦性能會有差異),分析opcode如下(截取主要部分):

 31           0 LOAD_CONST               1 (10000000)
              2 STORE_FAST               0 (l)

 33           4 BUILD_LIST               0
              6 STORE_FAST               1 (li)

 34           8 SETUP_LOOP              26 (to 36)
             10 LOAD_GLOBAL              0 (range)
             12 LOAD_FAST                0 (l)
             14 CALL_FUNCTION            1
             16 GET_ITER
        >>   18 FOR_ITER                14 (to 34)
             20 STORE_FAST               2 (i)

 35          22 LOAD_FAST                1 (li)
             24 LOAD_ATTR                1 (append)
             26 LOAD_FAST                2 (i)
             28 CALL_FUNCTION            1
             30 POP_TOP
             32 JUMP_ABSOLUTE           18
        >>   34 POP_BLOCK

由於循環量特別大,所以主要就是減少單次循環內的操作,這裏我們看到每次循環時都會進行一次LOAD_ATTR操作,即每次for循環都要查找一遍append屬性,於是我們可以先將for循環內的屬性查找提取出來,代碼如下:

import time

def test():
    l = 10000000
    start = time.time()
    li = []
    # 將load_attr操作提取出來
    app = li.append
    for i in range(l):
        # 這裏就不用再每次查找append屬性了
        app(i)
    print(time.time() - start)

test()
# 1.4092960357666016

可以看到效率有小幅度的增長,然後會發現好像沒有什麼可以優化的了...於是我們可以嘗試使用列表表達式來看看效果:

import time

def test():
    l = 10000000
    start = time.time()
    [i for i in range(l)]
    print(time.time() - start)

test()
# 1.1854212284088135

可以看到效率又有了一定的提升,這裏我們可以剖析一下爲什麼列表表達式效率比for循環的要快,首先我們看一下使用列表表達式對應的opcode

 4           0 LOAD_CONST               1 (10000000)
              2 STORE_FAST               0 (l)

 6           4 LOAD_CONST               2 (<code object <listcomp> at 0x000001CD2FEA4930, file "xxx.py", line 6>)
              6 LOAD_CONST               3 ('test.<locals>.<listcomp>')
              8 MAKE_FUNCTION            0
             10 LOAD_GLOBAL              0 (range)
             12 LOAD_FAST                0 (l)
             14 CALL_FUNCTION            1
             16 GET_ITER
             18 CALL_FUNCTION            1
             20 POP_TOP

可以看到列表表達式的本質是使用了一個listcomp函數來進行創建列表的操作,而listcomp對應的指令操作如下:

  1           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                 8 (to 14)
              6 STORE_FAST               1 (i)
              8 LOAD_FAST                1 (i)
             10 LIST_APPEND              2
             12 JUMP_ABSOLUTE            4
        >>   14 RETURN_VALUE

可以看到listcomp當中也是通過for循環迭代調用append函數將內容添加到一個列表當中,和我們使用for循環創建列表的操作看起來幾乎一樣,而差距的關鍵就在於列表表達式添加元素時使用的是LIST_APPEND指令,而我們調用append方法時使用的是CALL_FUNCTION指令調用append函數,LIST_APPEND指令操作源碼如下:

// 列表的append操作
case TARGET(LIST_APPEND): {
    PyObject *v = POP();
    PyObject *list = PEEK(oparg);
    int err;
    // 調用list提供的C接口PyList_Append來添加元素
    err = PyList_Append(list, v);
    Py_DECREF(v);
    if (err != 0)
        goto error;
    PREDICT(JUMP_ABSOLUTE);
    DISPATCH();
}

可以看到該指令直接調用了底層的PyList_Append函數接口對列表進行元素的添加,而我們使用的CALL_FUNCTION指令相對來說就十分繁瑣:雖然最終也是調用list提供的list_append接口(PyList_Appendlist_append的邏輯差不多),但在執行list_append之前卻要進行一系列的預操作,如:開闢、回收棧幀對象空間(PyList_Append是在C語言級別的棧空間開闢,而CALL_FUNCTION則是在Python棧幀對象級別上開闢)、參數接收、函數處理等一系列操作,無形中就降低了執行的效率,所以這就是爲什麼列表表達式比for循環更加快的原因

opcode優化參考

https://www.jianshu.com/p/f45e443cdfd7

函數

棧幀對象

在前面可以知道python程序會在模擬的函數棧對象當中不斷地執行字節碼,而這個棧對象的實現就是一個PyFrameObject,定義如下:

typedef struct _frame {
    PyObject_VAR_HEAD
    // 指向上一層棧幀
    struct _frame *f_back;      /* previous frame, or NULL */
    // 當前棧幀的code對象
    PyCodeObject *f_code;       /* code segment */
    // 當前棧幀的內置函數區
    PyObject *f_builtins;       /* builtin symbol table (PyDictObject) */
    // 當前棧幀的全局區(全局空間,xxx_global操作的空間,如load_global)
    PyObject *f_globals;        /* global symbol table (PyDictObject) */
    // 當前棧幀的局部區(命名空間,xxx_name操作的空間,如load_name)
    PyObject *f_locals;         /* local symbol table (any mapping) */
    // 指向棧底
    PyObject **f_valuestack;    /* points after the last local */
    /* Next free slot in f_valuestack.  Frame creation sets to f_valuestack.
       Frame evaluation usually NULLs it, but a frame that yields sets it
       to the current stack top. */
    // 指向棧頂
    PyObject **f_stacktop;
    PyObject *f_trace;          /* Trace function */
    char f_trace_lines;         /* Emit per-line trace events? */
    char f_trace_opcodes;       /* Emit per-opcode trace events? */

    /* Borrowed reference to a generator, or NULL */
    PyObject *f_gen;
    // 棧幀中上一條執行完指令的偏移
    int f_lasti;                /* Last instruction if called */
    /* Call PyFrame_GetLineNumber() instead of reading this field
       directly.  As of 2.3 f_lineno is only valid when tracing is
       active (i.e. when f_trace is set).  At other times we use
       PyCode_Addr2Line to calculate the line from the current
       bytecode index. */
    // 當前行
    int f_lineno;               /* Current line number */
    // 棧的索引
    int f_iblock;               /* index in f_blockstack */
    // 是否正在執行
    char f_executing;           /* whether the frame is still executing */
    PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
    // 動態內存,維護需要的空間(當前棧幀的棧空間,xxx_fast操作的空間,如load_fast)
    PyObject *f_localsplus[1];  /* locals+stack, dynamically sized */
} PyFrameObject;

我們可以通過sys模塊查看當前執行的棧幀,舉例:

import sys
import inspect

def test():
    # 獲取test函數執行棧幀
    a = sys._getframe()
    def aaa():
        b = sys._getframe()
        # 可以看到aaa函數的上一層棧幀就是test函數的棧幀
        print(b.f_back is a)
        # 當前調用堆棧的頂部棧幀就是aaa函數的棧幀
        print(inspect.stack()[0][0] is b)
        # 打印調用堆棧所對應的執行空間
        for stack in inspect.stack():
            print(stack.function)
    aaa()

test()

# True
# True
# aaa
# test
# <module>
函數本質

當創建函數時,函數對象就會綁定相關的執行接口_PyFunction_Vectorcall,當函數被調用時,就是調用_PyFunction_Vectorcall函數,函數中將會創建對應的棧幀對象,然後在該棧幀下依次執行函數指向的code對象裏的字節指令(即調用PyEval_EvalFrameEx函數,在巨大的switch裏執行指令),源碼如下:

// 函數調用接口
PyObject *
_PyFunction_Vectorcall(PyObject *func, PyObject* const* stack,
                       size_t nargsf, PyObject *kwnames)
{
    // 獲取code對象
    PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func);
    PyObject *globals = PyFunction_GET_GLOBALS(func);
    // 獲取參數默認值
    PyObject *argdefs = PyFunction_GET_DEFAULTS(func);
    PyObject *kwdefs, *closure, *name, *qualname;
    PyObject **d;
    Py_ssize_t nkwargs = (kwnames == NULL) ? 0 : PyTuple_GET_SIZE(kwnames);
    Py_ssize_t nd;

    assert(PyFunction_Check(func));
    Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
    assert(nargs >= 0);
    assert(kwnames == NULL || PyTuple_CheckExact(kwnames));
    assert((nargs == 0 && nkwargs == 0) || stack != NULL);
    /* kwnames must only contains str strings, no subclass, and all keys must
       be unique */

    if (co->co_kwonlyargcount == 0 && nkwargs == 0 &&
        (co->co_flags & ~PyCF_MASK) == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE))
    {
        if (argdefs == NULL && co->co_argcount == nargs) {
            // 函數調用的快速通道,內部將會調用PyEval_EvalFrameEx
            return function_code_fastcall(co, stack, nargs, globals);
        }
        else if (nargs == 0 && argdefs != NULL
                 && co->co_argcount == PyTuple_GET_SIZE(argdefs)) {
            /* function called with no arguments, but all parameters have
               a default value: use default values as arguments .*/
            stack = _PyTuple_ITEMS(argdefs);
            return function_code_fastcall(co, stack, PyTuple_GET_SIZE(argdefs),
                                          globals);
        }
    }
    // 有鍵參數的情況
    kwdefs = PyFunction_GET_KW_DEFAULTS(func);
    closure = PyFunction_GET_CLOSURE(func);
    name = ((PyFunctionObject *)func) -> func_name;
    qualname = ((PyFunctionObject *)func) -> func_qualname;

    if (argdefs != NULL) {
        d = _PyTuple_ITEMS(argdefs);
        nd = PyTuple_GET_SIZE(argdefs);
    }
    else {
        d = NULL;
        nd = 0;
    }
    // 執行函數中的code內容,內部將會調用PyEval_EvalFrameEx
    return _PyEval_EvalCodeWithName((PyObject*)co, globals, (PyObject *)NULL,
                                    stack, nargs,
                                    nkwargs ? _PyTuple_ITEMS(kwnames) : NULL,
                                    stack + nargs,
                                    nkwargs, 1,
                                    d, (int)nd, kwdefs,
                                    closure, name, qualname);
}

// 函數調用快速通道主邏輯
static PyObject* _Py_HOT_FUNCTION
function_code_fastcall(PyCodeObject *co, PyObject *const *args, Py_ssize_t nargs,
                       PyObject *globals)
{
    PyFrameObject *f;
    // 獲取當前線程
    PyThreadState *tstate = _PyThreadState_GET();
    PyObject **fastlocals;
    Py_ssize_t i;
    PyObject *result;

    assert(globals != NULL);
    /* XXX Perhaps we should create a specialized
       _PyFrame_New_NoTrack() that doesn't take locals, but does
       take builtins without sanity checking them.
       */
    assert(tstate != NULL);
    // 創建一個棧幀對象
    f = _PyFrame_New_NoTrack(tstate, co, globals, NULL);
    if (f == NULL) {
        return NULL;
    }

    fastlocals = f->f_localsplus;

    for (i = 0; i < nargs; i++) {
        Py_INCREF(*args);
        fastlocals[i] = *args++;
    }
    // 獲取執行結果
    result = PyEval_EvalFrameEx(f,0);

    if (Py_REFCNT(f) > 1) {
        Py_DECREF(f);
        _PyObject_GC_TRACK(f);
    }
    else {
        // 遞歸深度控制
        ++tstate->recursion_depth;
        Py_DECREF(f);
        --tstate->recursion_depth;
    }
    return result;
}

所以執行函數的本質就是執行code對象裏的字節指令,例如我們可以通過修改函數的code對象來修改函數的功能,舉例:

def test1(a=1):
    print("test1", a)

def test2(a=100):
    print("test2", a)

# 修改test1的code對象
test1.__code__ = test2.__code__

test1()

# test2 1

可以看到test1最終執行的是test2的指令,但因爲這裏我們只是進行了code對象的修改(即只是修改了執行的指令),而默認參數空間還是用函數test1對象的,所以輸出的a結果是1而不是100,這樣我們就悄悄地實現了對函數的“移花接木”

閉包

例如下面的函數,test函數嵌套了func函數,func是閉包函數:

def test(a = 2, b = 3):
    c = "aaa"
    d = 1
    e = 5
    def func():
        nonlocal a
        print(locals())
        b
        c
        d = 4
    return func

clo = test()
clo()

# {'c': 'aaa', 'b': 3, 'a': 2}

其中函數test的字節碼:

  2           0 LOAD_CONST               1 ('aaa')
              2 STORE_DEREF              2 (c)

  3           4 LOAD_CONST               2 (1)
              6 STORE_FAST               2 (d)

  4           8 LOAD_CONST               3 (5)
             10 STORE_FAST               3 (e)

  5          12 LOAD_CLOSURE             0 (a)
             14 LOAD_CLOSURE             1 (b)
             16 LOAD_CLOSURE             2 (c)
             18 BUILD_TUPLE              3
             20 LOAD_CONST               4 (<code object func at 0x000001D669AB49C0, file "xxx.py", line 5>)
             22 LOAD_CONST               5 ('test.<locals>.func')
             24 MAKE_FUNCTION            8
             26 STORE_FAST               4 (func)

 11          28 LOAD_FAST                4 (func)
             30 RETURN_VALUE

通過5的前幾行可以看出,閉包裏載入了變量a/b/c,而d因爲在閉包函數裏將會進行賦值操作,所以閉包函數會默認認爲d是在閉包函數內部創建的,因此不需要從外層函數載入,變量e則是因爲沒有用到,因此也就沒必要進行載入,閉包存儲的數據可以在閉包函數裏通過locals()獲取,但裏面會包括函數裏的其他局部變量,如果希望只獲取外部載入閉包的相關內容,可以通過__closure__屬性獲取,舉例:

def test(a = 2, b = 3):
    c = 5
    d = 1
    e = 5
    def func():
        nonlocal a
        print(locals())
        b
        c
        d = 4
        print(locals())
    return func

clo = test()
print([each.cell_contents for each in clo.__closure__])
# 輸出所有閉包包含的內容
clo()

# [2, 3, 5]
# {'c': 5, 'b': 3, 'a': 2}
# {'c': 5, 'b': 3, 'a': 2, 'd': 4}
函數參數限制

在python3.6之前,由於函數的參數數量是通過2字節的oparg來進行記錄的,其中低8位用於記錄位置參數的數量,高8位用於記錄鍵參數的數量,因此參數數量不允許超過255,獲取參數邏輯如下:

// ~/python/ceval.c
// 3.6之前的獲取參數方式
static PyObject *
call_function(PyObject ***pp_stack, int oparg
#ifdef WITH_TSC
        , uint64* pintr0, uint64* pintr1
#endif
        )
{
    // 低8位是位置參數數量
    int na = oparg & 0xff;
    // 高8位是鍵參數數量
    int nk = (oparg>>8) & 0xff;
    int n = na + 2 * nk;
    ...
}

因此如果參數超過255,那麼獲取參數數量時就會出現問題,所以python在解析代碼時,如果超過255個參數就會拋出語法異常,舉例:

def aaa(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32, a33, a34, a35, a36, a37, a38, a39, a40, a41, a42, a43, a44, a45, a46, a47, a48, a49, a50, a51, a52, a53, a54, a55, a56, a57, a58, a59, a60, a61, a62, a63, a64, a65, a66, a67, a68, a69, a70, a71, a72, a73, a74, a75, a76, a77, a78, a79, a80, a81, a82, a83, a84, a85, a86, a87, a88, a89, a90, a91, a92, a93, a94, a95, a96, a97, a98, a99, a100, a101, a102, a103, a104, a105, a106, a107, a108, a109, a110, a111, a112, a113, a114, a115, a116, a117, a118, a119, a120, a121, a122, a123, a124, a125, a126, a127, a128, a129, a130, a131, a132, a133, a134, a135, a136, a137, a138, a139, a140, a141, a142, a143, a144, a145, a146, a147, a148, a149, a150, a151, a152, a153, a154, a155, a156, a157, a158, a159, a160, a161, a162, a163, a164, a165, a166, a167, a168, a169, a170, a171, a172, a173, a174, a175, a176, a177, a178, a179, a180, a181, a182, a183, a184, a185, a186, a187, a188, a189, a190, a191, a192, a193, a194, a195, a196, a197, a198, a199, a200, a201, a202, a203, a204, a205, a206, a207, a208, a209, a210, a211, a212, a213, a214, a215, a216, a217, a218, a219, a220, a221, a222, a223, a224, a225, a226, a227, a228, a229, a230, a231, a232, a233, a234, a235, a236, a237, a238, a239, a240, a241, a242, a243, a244, a245, a246, a247, a248, a249, a250, a251, a252, a253, a254, a255):
    pass

# SyntaxError: more than 255 arguments

注意這裏是在語法解析層面進行的檢查,而不是在函數執行的時候進行檢查,解析源碼如下:

// ~/python/ast.c
static expr_ty
ast_for_call(struct compiling *c, const node *n, expr_ty func)
{
    ...

    nargs = 0;
    nkeywords = 0;
    ngens = 0;
    ...
    // 參數總數不允許超過255個
    if (nargs + nkeywords + ngens > 255) {
      ast_error(n, "more than 255 arguments");
      return NULL;
    }
    ...

python3.7中爲了解決oparg的限制問題,當oparg超過255時,會通過EXTENDED_ARG指令來進行擴充,指令源碼如下:

// 當oparg不夠裝的時候(例如oparg>=255),對oparg加8個字節進行擴充
// 例如:LOAD_NAME 257是不能直接執行的,因爲oparg上限是255,因此就會轉換成執行下面指令:
// EXTENDED_ARG 1
// LOAD_NAME 1
// 解釋:EXTENDED_ARG會將oparg先變成256(00000001 00000000),並取下一個指令的opcode和oparg,
// 然後256與下一個的oparg進行或運算,例如這裏就變成256 | 1,結果就是257,所以最終就執行了:LOAD_NAME 257
case TARGET(EXTENDED_ARG): {
    int oldoparg = oparg;
    NEXTOPARG();
    oparg |= oldoparg << 8;
    goto dispatch_opcode;
}

還是對前面那個函數參數超過255的示例,我們可以查看opcode的操作:

...
510 LOAD_NAME              255 (a254)
512 EXTENDED_ARG             1
514 LOAD_NAME              256 (a255)
516 EXTENDED_ARG             1
518 CALL_FUNCTION          256
...

這裏可以看出oparg已經可以超過255了,而對應的字節碼如下:

[..., 144, 1, 101, 0, 144, 1, 131, 0, ...]
# 144代表EXTENDED_ARG,101代表LOAD_NAME,131代表CALL_FUNCTION

[144, 1, 131, 0]就可以解析爲:

EXTENDED_ARG             1
CALL_FUNCTION          0

即:

CALL_FUNCTION          256

並且在python3.7以後也刪除了對應的語法解析,所以之後對於參數數量就不再有255長度的限制了

類機制

mro機制

python中的類對象支持多繼承,而在多繼承當中,確認類的繼承順序是件十分複雜的問題,在python中就採用了C3算法來確認繼承順序,源碼如下:

// 確認mro順序的邏輯
static PyObject *
mro_implementation(PyTypeObject *type)
{
    PyObject *result;
    PyObject *bases;
    PyObject **to_merge;
    Py_ssize_t i, n;

    if (type->tp_dict == NULL) {
        if (PyType_Ready(type) < 0)
            return NULL;
    }

    bases = type->tp_bases;
    assert(PyTuple_Check(bases));
    n = PyTuple_GET_SIZE(bases);
    // 直接父類的mro存在確認
    for (i = 0; i < n; i++) {
        PyTypeObject *base = (PyTypeObject *)PyTuple_GET_ITEM(bases, i);
        if (base->tp_mro == NULL) {
            PyErr_Format(PyExc_TypeError,
                         "Cannot extend an incomplete type '%.100s'",
                         base->tp_name);
            return NULL;
        }
        assert(PyTuple_Check(base->tp_mro));
    }
    // 單繼承的情況,直接將當前類+父類的mro組成元組返回
    if (n == 1) {
        /* Fast path: if there is a single base, constructing the MRO
         * is trivial.
         */
        PyTypeObject *base = (PyTypeObject *)PyTuple_GET_ITEM(bases, 0);
        Py_ssize_t k = PyTuple_GET_SIZE(base->tp_mro);
        result = PyTuple_New(k + 1);
        if (result == NULL) {
            return NULL;
        }
        Py_INCREF(type);
        PyTuple_SET_ITEM(result, 0, (PyObject *) type);
        for (i = 0; i < k; i++) {
            PyObject *cls = PyTuple_GET_ITEM(base->tp_mro, i);
            Py_INCREF(cls);
            PyTuple_SET_ITEM(result, i + 1, cls);
        }
        return result;
    }

    /* This is just a basic sanity check. */
    if (check_duplicates(bases) < 0) {
        return NULL;
    }

    /* Find a superclass linearization that honors the constraints
       of the explicit tuples of bases and the constraints implied by
       each base class.

       to_merge is an array of tuples, where each tuple is a superclass
       linearization implied by a base class.  The last element of
       to_merge is the declared tuple of bases.
    */
    // 多繼承
    to_merge = PyMem_New(PyObject *, n + 1);
    if (to_merge == NULL) {
        PyErr_NoMemory();
        return NULL;
    }
    // 將所有直接父類的mro列表存到merge列表當中
    for (i = 0; i < n; i++) {
        PyTypeObject *base = (PyTypeObject *)PyTuple_GET_ITEM(bases, i);
        to_merge[i] = base->tp_mro;
    }
    // 將直接父類列表頁存到merge列表當中
    // 例如A(B, C)就變成:[orm(B), orm(C), [B, C]]
    to_merge[n] = bases;

    result = PyList_New(1);
    if (result == NULL) {
        PyMem_Del(to_merge);
        return NULL;
    }

    Py_INCREF(type);
    PyList_SET_ITEM(result, 0, (PyObject *)type);
    // 對merge列表進行merge操作
    if (pmerge(result, to_merge, n + 1) < 0) {
        Py_CLEAR(result);
    }

    PyMem_Del(to_merge);
    return result;
}

// C3算法的merge邏輯
static int
pmerge(PyObject *acc, PyObject **to_merge, Py_ssize_t to_merge_size)
{
    int res = 0;
    Py_ssize_t i, j, empty_cnt;
    int *remain;

    /* remain stores an index into each sublist of to_merge.
       remain[i] is the index of the next base in to_merge[i]
       that is not included in acc.
    */
    remain = PyMem_New(int, to_merge_size);
    if (remain == NULL) {
        PyErr_NoMemory();
        return -1;
    }
    // 設置每個merge組中取出了n個以後的起始位置
    // (例如[[A,B], [A,C]],起始是remain爲:[0, 0],如果A被取出並加入到了mro列表,那麼remain就變成了[1,1],那麼下一次這兩組都是從第二個開始找候選)
    for (i = 0; i < to_merge_size; i++)
        remain[i] = 0;

  again:
    empty_cnt = 0;
    for (i = 0; i < to_merge_size; i++) {
        PyObject *candidate;

        PyObject *cur_tuple = to_merge[i];
        // 如果當前組的類都取出了,則從下一個組中尋找候選
        if (remain[i] >= PyTuple_GET_SIZE(cur_tuple)) {
            empty_cnt++;
            continue;
        }

        /* Choose next candidate for MRO.

           The input sequences alone can determine the choice.
           If not, choose the class which appears in the MRO
           of the earliest direct superclass of the new class.
        */
        // 選出當前遍歷到的那一組的未被取出的第一個作爲候選人
        candidate = PyTuple_GET_ITEM(cur_tuple, remain[i]);
        // 如果該候選人在別的組裏的尾部(非第一個未被取出的)存在,則尋找下一個候選人
        for (j = 0; j < to_merge_size; j++) {
            PyObject *j_lst = to_merge[j];
            if (tail_contains(j_lst, remain[j], candidate))
                goto skip; /* continue outer loop */
        }
        // 如果是符合條件的候選,則添加進mro列表
        res = PyList_Append(acc, candidate);
        if (res < 0)
            goto out;
        // 將所有組中的該候選取出(候選的索引起始位置變成該候選的位置+1)
        for (j = 0; j < to_merge_size; j++) {
            PyObject *j_lst = to_merge[j];
            if (remain[j] < PyTuple_GET_SIZE(j_lst) &&
                PyTuple_GET_ITEM(j_lst, remain[j]) == candidate) {
                remain[j]++;
            }
        }
        goto again;
      skip: ;
    }
    // 能到這則merge中所有的組裏的類都必須全部取出
    if (empty_cnt != to_merge_size) {
        set_mro_error(to_merge, to_merge_size, remain);
        res = -1;
    }

  out:
    PyMem_Del(remain);

    return res;
}
查找屬性

類對象的屬性查找的默認邏輯源碼如下:

// 類對象getattr主邏輯
// 尋找順序:元類數據描述符->基類mro上屬性->元類屬性
static PyObject *
type_getattro(PyTypeObject *type, PyObject *name)
{   
    // 獲取對象的元類
    PyTypeObject *metatype = Py_TYPE(type);
    PyObject *meta_attribute, *attribute;
    descrgetfunc meta_get;
    PyObject* res;

    if (!PyUnicode_Check(name)) {
        PyErr_Format(PyExc_TypeError,
                     "attribute name must be string, not '%.200s'",
                     name->ob_type->tp_name);
        return NULL;
    }

    /* Initialize this type (we'll assume the metatype is initialized) */
    if (type->tp_dict == NULL) {
        if (PyType_Ready(type) < 0)
            return NULL;
    }

    /* No readable descriptor found yet */
    meta_get = NULL;

    /* Look for the attribute in the metatype */
    // 在元類上尋找對應的屬性
    meta_attribute = _PyType_Lookup(metatype, name);
    // 如果元類中存在該屬性
    if (meta_attribute != NULL) {
        Py_INCREF(meta_attribute);
        // 獲取屬性的__get__方法
        meta_get = Py_TYPE(meta_attribute)->tp_descr_get;
        // 如果是數據描述符(__get__和__set__方法都有)
        if (meta_get != NULL && PyDescr_IsData(meta_attribute)) {
            /* Data descriptors implement tp_descr_set to intercept
             * writes. Assume the attribute is not overridden in
             * type's tp_dict (and bases): call the descriptor now.
             */
            // 獲取執行對應__get__方法的返回值
            res = meta_get(meta_attribute, (PyObject *)type,
                           (PyObject *)metatype);
            Py_DECREF(meta_attribute);
            return res;
        }
    }

    /* No data descriptor found on metatype. Look in tp_dict of this
     * type and its bases */
    // 去mro列表中依次尋找__dict__中是否有對應屬性
    attribute = _PyType_Lookup(type, name);
    // 如果屬性存在
    if (attribute != NULL) {
        /* Implement descriptor functionality, if any */
        Py_INCREF(attribute);
        descrgetfunc local_get = Py_TYPE(attribute)->tp_descr_get;

        Py_XDECREF(meta_attribute);
        // 如果是描述符,則返回描述符的執行結果
        if (local_get != NULL) {
            /* NULL 2nd argument indicates the descriptor was
             * found on the target object itself (or a base)  */
            
            res = local_get(attribute, (PyObject *)NULL,
                            (PyObject *)type);
            Py_DECREF(attribute);
            return res;
        }
        // 否則直接返回屬性
        return attribute;
    }

    /* No attribute found in local __dict__ (or bases): use the
     * descriptor from the metatype, if any */
    // 如果元類上對應name是非數據描述符
    if (meta_get != NULL) {
        PyObject *res;
        // 執行對應__get__方法獲取返回值
        res = meta_get(meta_attribute, (PyObject *)type,
                       (PyObject *)metatype);
        Py_DECREF(meta_attribute);
        return res;
    }

    /* If an ordinary attribute was found on the metatype, return it now */
    // 如果元類上對應name如果不是描述符,則直接返回屬性
    if (meta_attribute != NULL) {
        return meta_attribute;
    }

    /* Give up */
    PyErr_Format(PyExc_AttributeError,
                 "type object '%.50s' has no attribute '%U'",
                 type->tp_name, name);
    return NULL;
}
載入屬性

在對象中調用屬性時,將會執行LOAD_ATTR指令查找對應的屬性(指令會調用PyObject_GetAttr進行查找),因此會有一定的性能損耗,舉例:

from time import time

class A:
    def run(self):
        pass

l = 100000000
a = A()

def count_time(func):
    def wrapper():
        s = time()
        func()
        print(func.__name__, ":", time() - s)
    wrapper()

@count_time
def get_attr_once():
    """只獲取一次方法屬性"""
    ar = a.run
    for i in range(l):
        ar

@count_time
def get_attr_every():
    """每次都獲取一次方法屬性"""
    for i in range(l):
        a.run

# get_attr_once : 4.7732908725738525
# get_attr_every : 11.221587419509888

其中LOAD_ATTR指令源碼如下:

// LOAD_ATTR操作
case TARGET(LOAD_ATTR): {
    PyObject *name = GETITEM(names, oparg);
    PyObject *owner = TOP();
    // 查找對象屬性
    PyObject *res = PyObject_GetAttr(owner, name);
    Py_DECREF(owner);
    SET_TOP(res);
    if (res == NULL)
        goto error;
    DISPATCH();
}

...

// getattr邏輯
PyObject *
PyObject_GetAttr(PyObject *v, PyObject *name)
{
    PyTypeObject *tp = Py_TYPE(v);

    if (!PyUnicode_Check(name)) {
        PyErr_Format(PyExc_TypeError,
                     "attribute name must be string, not '%.200s'",
                     name->ob_type->tp_name);
        return NULL;
    }
    if (tp->tp_getattro != NULL)
        return (*tp->tp_getattro)(v, name);
    if (tp->tp_getattr != NULL) {
        const char *name_str = PyUnicode_AsUTF8(name);
        if (name_str == NULL)
            return NULL;
        // 通過對象的getattr方法查找屬性
        return (*tp->tp_getattr)(v, (char *)name_str);
    }
    PyErr_Format(PyExc_AttributeError,
                 "'%.50s' object has no attribute '%U'",
                 tp->tp_name, name);
    return NULL;
}
方法中self的由來

調用實例對象方法時我們明明沒有傳入self,那方法是如何獲取到其對應的實例對象self的呢?由於方法是一個method類,而在method類中維護着幾個重要的屬性:

__self__  調用該方法的實例對象
__func__  方法對應的執行函數

因此在調用實例方法時,method類會調用其維護的__func__函數,並將__self__對象作爲第一個參數傳入,舉例:

class A:
    def run(self):
        print(self)

def test():
    a = A()
    a.run()
    # 上一句的本質
    a.run.__func__(a.run.__self__)
    # 和上面的也等價
    # 因爲a.run.__func__指向的就是A.run函數,而a.run.__self__指向的就是a
    A.run(a)

test()

# <__main__.A object at 0x00000287114D4780>
# <__main__.A object at 0x00000287114D4780>
# <__main__.A object at 0x00000287114D4780>

而這個調用方式我們也可以在python源碼當中得到證明:

// ~/Objects/classobject.c

// method對象定義
typedef struct {
    PyObject_HEAD
    // 綁定的函數
    PyObject *im_func;   /* The callable object implementing the method */
    // 綁定的對象
    PyObject *im_self;   /* The instance it is bound to */
    // 弱引用列表
    PyObject *im_weakreflist; /* List of weak references */
    // method的調用方式(獲取對應的函數和對象,然後再進行調用)
    vectorcallfunc vectorcall;
} PyMethodObject;

// 創建一個方法類,會傳入綁定的函數和對象
PyObject *
PyMethod_New(PyObject *func, PyObject *self)
{
    PyMethodObject *im;
    if (self == NULL) {
        PyErr_BadInternalCall();
        return NULL;
    }
    // 緩衝池操作
    im = free_list;
    if (im != NULL) {
        free_list = (PyMethodObject *)(im->im_self);
        (void)PyObject_INIT(im, &PyMethod_Type);
        numfree--;
    }
    else {
        im = PyObject_GC_New(PyMethodObject, &PyMethod_Type);
        if (im == NULL)
            return NULL;
    }
    // 初始化設置
    im->im_weakreflist = NULL;
    Py_INCREF(func);
    // 綁定的函數
    im->im_func = func;
    Py_XINCREF(self);
    // 綁定的對象
    im->im_self = self;
    // 調用method類時的處理函數
    im->vectorcall = method_vectorcall;
    _PyObject_GC_TRACK(im);
    return (PyObject *)im;
}

// method類的調用方式
static PyObject *
method_vectorcall(PyObject *method, PyObject *const *args,
                  size_t nargsf, PyObject *kwnames)
{
    assert(Py_TYPE(method) == &PyMethod_Type);
    PyObject *self, *func, *result;
    // 獲取綁定的對象和函數
    self = PyMethod_GET_SELF(method);
    func = PyMethod_GET_FUNCTION(method);
    // 獲取參數數量
    Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
    // 如果有參數,則將self插入到第一個參數前面
    if (nargsf & PY_VECTORCALL_ARGUMENTS_OFFSET) {
        /* PY_VECTORCALL_ARGUMENTS_OFFSET is set, so we are allowed to mutate the vector */
        // 找到第一個參數的前一個位置空間
        PyObject **newargs = (PyObject**)args - 1;
        // 參數數量+1
        nargs += 1;
        // 將第一個位置的參數(實際上是原本第一個參數的上一個位置)設置爲self
        PyObject *tmp = newargs[0];
        newargs[0] = self;
        // 調用函數
        result = _PyObject_Vectorcall(func, newargs, nargs, kwnames);
        // 恢復原來的內存佈局
        newargs[0] = tmp;
    }
    else {
        // 鍵參數數量
        Py_ssize_t nkwargs = (kwnames == NULL) ? 0 : PyTuple_GET_SIZE(kwnames);
        // 總的參數數量
        Py_ssize_t totalargs = nargs + nkwargs;
        // 沒有參數,則直接將self傳入
        if (totalargs == 0) {
            return _PyObject_Vectorcall(func, &self, 1, NULL);
        }
        // 有參數則申請空間來存放參數
        PyObject *newargs_stack[_PY_FASTCALL_SMALL_STACK];
        PyObject **newargs;
        if (totalargs <= (Py_ssize_t)Py_ARRAY_LENGTH(newargs_stack) - 1) {
            newargs = newargs_stack;
        }
        else {
            newargs = PyMem_Malloc((totalargs+1) * sizeof(PyObject *));
            if (newargs == NULL) {
                PyErr_NoMemory();
                return NULL;
            }
        }
        /* use borrowed references */
        // 依舊第一個位置的參數爲self
        newargs[0] = self;
        /* bpo-37138: since totalargs > 0, it's impossible that args is NULL.
         * We need this, since calling memcpy() with a NULL pointer is
         * undefined behaviour. */
        assert(args != NULL);
        // 將剩下的參數放在第一個參數的後面
        memcpy(newargs + 1, args, totalargs * sizeof(PyObject *));
        result = _PyObject_Vectorcall(func, newargs, nargs+1, kwnames);
        if (newargs != newargs_stack) {
            PyMem_Free(newargs);
        }
    }
    return result;
}
類方法/成員方法/靜態方法比較

類方法在創建完類之後就會將函數與當前類進行綁定,變成一個方法對象;成員方法則是在實例化時將函數與當前對象進行綁定,變成一個方法對象;而靜態方法則是一直都只是函數對象,不綁定任何對象,舉例:

class A:
    @classmethod
    def xxx(cls):
        pass
    def yyy(self):
        pass
    @staticmethod
    def zzz():
        pass

print(type(A.xxx), A.xxx.__self__)
print(type(A.yyy))
print(type(A.zzz))

a = A()
print(type(a.xxx), a.xxx.__self__)
print(type(a.yyy), a.yyy.__self__)
print(type(a.zzz))

# <class 'method'> <class '__main__.A'>
# <class 'function'>
# <class 'function'>
# <class 'method'> <class '__main__.A'>
# <class 'method'> <__main__.A object at 0x00000186AE62D198>
# <class 'function'>
type獲取元類本質

每個對象都有一個__class__屬性用於記錄實例化當前對象的類,而type獲取類就是獲取的該屬性,證明如下:

class A: pass
class B: pass

a = A()
# 改變實例對象a指向的類
a.__class__ = B

# 可以看出type輸出的是B類
print(type(a), type(a) is a.__class__)

# <class '__main__.B'> True

而對於一個對象,其屬性的查找方式有一部分也是基於__class__屬性來實現(先找到實例自身裏查找,如果沒有,再通過__class__指向的類的mro順序進行查找),證明如下:

class A:
    def __init__(self):
        self.x = 1
    def run(self):
        print(1)

class B: pass

a = A()
a.__class__ = B

# x是實例對象中的屬性,所以可以獲取到
print(a.x)
# run是A類裏的屬性,因爲指向的類變成了B,所以無法再找到run屬性了
a.run(1)

# 1
# AttributeError: 'B' object has no attribute 'run'
類對象和實例對象下的描述符

當描述符作爲類對象的屬性時,將會按照描述符的方式去解析,但放到實例對象當中時,就會變成一個單純的對象,證明如下:

class D:
    def __init__(self):
        self.x = 0
    def __get__(self, i, o):
        return self.x
    def __set__(self, i, v):
        self.x = v

class A:
    x = D()
    def __init__(self):
        self.y = D()

a = A()
print(a.x, a.y, a.__dict__)

# 0 <__main__.D object at 0x000001E734A54A20> {'y': <__main__.D object at 0x000001E734A54A20>}

可以看到x作爲類屬性,返回的是通過__get__方法獲取的值,而y作爲實例屬性,返回的只是一個對象

命名空間和作用域

搜索規則

命名空間和作用域參考:https://blog.csdn.net/qq_38329988/article/details/88667825

LGB規則

命名空間下查詢變量時,遵循:local -> global -> builtin的順序,即LGB規則(如果存在閉包,則遵循LEGB規則,其中E是Enclosing),舉例:

>>> list
<class 'list'>
# 查找到的是builtin裏的list
>>> list = 1
# 在local定義list(全局區的local和global指向的是同一個)
>>> list
1
# 獲取的是local的list
>>> del list
# 刪除的是local的list
>>> list
<class 'list'>
# 可以看到再次獲取到了builtin中的list
>>> __builtins__.list
<class 'list'>
>>> __builtins__.list = 1
# 修改builtin中的list,會導致很多內部對list的使用出問題,程序崩潰
>>> list
1
Exception ignored in: 
...
命名操作規則

對命名進行的操作如果會影響命名空間發生改變,那麼操作將作用於局部作用域,例如賦值操作、刪除操作等,舉例:

>>> globals() == locals()
True
# 模塊下函數外的全局作用域和局部作用域指向的是同一個
>>> del list
NameError: name 'list' is not defined
# del操作會觸發命名空間改變,因此對局部作用域進行操作
# 因爲局部作用域下沒有list,所以刪除失敗(list在內置作用域)
>>> a = []
>>> def test():
    del a
>>> test()
UnboundLocalError: local variable 'a' referenced before assignment
# 對命名進行操作,因此只查找test函數下的局部變量,顯然a不存在(a在全局作用域)
>>> a.append(1)
>>> def test():
    del a[0]
>>> test()
# 對引用類型內部進行操作,命名空間不產生變化,因此操作成功
>>> a
[]

再比如下面的例子:

a = 100

def test1():
    print(a)

def test2():
    print(a)
    a = 1

test1()
test2()

# 100
# UnboundLocalError: local variable 'a' referenced before assignment

可以看到同樣是輸出a,但test2卻報錯提示a沒有定義,這就是因爲test2中存在對a的賦值語句,因此解析時認爲a是存在於test2局部的,從而導致錯誤的發生。如果希望能夠使用全局作用域上的a,那麼可以加globals關鍵字,代表強制對全局中的a進行操作,而不用遵守LGB規則

再比如下面示例:

# other.py代碼:

# a = 1

# def test():
#     print(a)

from other import a, test
# 雖然導入了a,但a=100會對當前命名空間產生影響,所以操作的是當前命名空間下的局部變量,而不是模塊下的a變量
a = 100
test()

import other
# 修改的是模塊下的屬性,對當前命名空間不會產生影響
other.a = 1000
test()

# 1
# 1000
全局區的local/global關係

由於全局區存放的變量都是全局的,也就不存在局部變量一說,因此在全局區裏,locals()globals()指向的是同一個對象,證明如下:

>>> globals() is locals()
True
locals()原理

局部變量存儲在當前函數執行環境的棧空間裏,因此在對應的棧幀對象中,有一個f_locals屬性會指向當前棧幀裏的局部變量字典,而locals函數的本質也就是從當前棧幀當中獲取f_locals屬性,證明如下:

import sys

def test():
    f = sys._getframe()
    print(locals() is f.f_locals)

test()
# True

源碼邏輯如下:

// locals邏輯
PyObject *
PyEval_GetLocals(void)
{
    // 獲取當前執行的線程對象
    PyThreadState *tstate = _PyThreadState_GET();
    // 獲取當前執行的棧幀對象
    PyFrameObject *current_frame = _PyEval_GetFrame(tstate);
    if (current_frame == NULL) {
        _PyErr_SetString(tstate, PyExc_SystemError, "frame does not exist");
        return NULL;
    }

    if (PyFrame_FastToLocalsWithError(current_frame) < 0) {
        return NULL;
    }

    assert(current_frame->f_locals != NULL);
    // 返回棧幀指向的f_locals
    return current_frame->f_locals;
}

同理,函數執行時對應的棧幀對象中也會將全局變量的指向存放到f_globals屬性裏,因此globals()函數也就是獲取當前棧幀的f_globals屬性

模塊

import機制

import時會先判斷sys.modules中是否存在該模塊,如果存在則直接將該模塊返回,否則從sys.path的路徑當中尋找對應的模塊進行導入,舉例:

import sys
import random
m1 = random

import random
print(m1 is random)

# True

可以看出第二次導入的模塊和第一次導入的是同一個。所以如果希望重新載入一次模塊,那麼可以將sys.module的對應模塊刪除,從而重新導入(但不代表之前的模塊資源會被釋放,例如下面的示例中變量m1引用了之前導入的模塊,那麼引用不爲0,不會被釋放,所以需要注意內存泄露的問題),舉例:

import sys
import random
m1 = random

# 刪除模塊,使其能夠重新導入
del sys.modules["random"]

import random
print(m1 is random)

# False

線程

GIL機制

python中所有的多線程實際上只能併發執行,即同一時間裏,只能有一個線程在真正的運行,而之所以這樣,就是因爲python的多線程當中有一把大鎖gil,用來控制當前允許執行的線程。

  • 爲什麼要有gil:由於python的內存管理主要是靠引用計數來完成了,爲了避免資源的使用異常(計數期間,如果調度到別的線程,則很可能引發一些意外的情況,如:A線程使用的資源在B線程被釋放、資源應該釋放時卻沒有被正確回收),所以通過gil限制了不允許多個線程同時執行

  • 既然是爲了保證引用計數的正確,爲什麼gil不只對引用計數的部分進行上鎖:首先在源碼當中,引用計數的代碼特別多,只對該部分進行gil控制,那麼代碼將可能變得難以維護,並且上鎖和釋放鎖是十分消耗資源的,頻繁地進行這些操作,反而會降低性能

  • gil控制線程切換方式:主要通過_PyThreadState_Swap方法更新當前佔用gil的線程,源碼如下:

// 更新當前獲得GIL鎖的線程(切換線程)
PyThreadState *
_PyThreadState_Swap(struct _gilstate_runtime_state *gilstate, PyThreadState *newts)
{
    // 當前獲取gil的線程
    PyThreadState *oldts = _PyRuntimeGILState_GetThreadState(gilstate);
    // 設置新的獲取GIL的線程
    _PyRuntimeGILState_SetThreadState(gilstate, newts);
    /* It should not be possible for more than one thread state
       to be used for a thread.  Check this the best we can in debug
       builds.
    */
#if defined(Py_DEBUG)
    if (newts) {
        /* This can be called from PyEval_RestoreThread(). Similar
           to it, we need to ensure errno doesn't change.
        */
        int err = errno;
        PyThreadState *check = _PyGILState_GetThisThreadState(gilstate);
        if (check && check->interp == newts->interp && check != newts)
            Py_FatalError("Invalid thread state for this thread");
        errno = err;
    }
#endif
    return oldts;
}
調度切換機制

Python在進行多線程編程時,會維護一個時間片數值N,當執行了指定長的時間片後就會進行線程的切換,我們可以通過sys模塊下getswitchinterval/setswitchinterval方法來進行查看和設置(python2中是維護執行指令的個數,即根據執行指令個數來進行線程切換,使用的是getcheckinterval/setcheckinterval方法來進行查看和設置),舉例:

>>> import sys
>>> sys.getswitchinterval()
0.005
# 默認是5毫秒
>>> sys.setswitchinterval(1)
>>> sys.getswitchinterval()
1.0

例如下面的代碼,我們就可以通過修改時間片來查看運行的結果:

import threading
import sys

# 可以對比執行這句和不執行這句的差別(默認0.005秒,改爲1秒)
# sys.setswitchinterval(1)

total = 0
li = []

def test(n):
    global total
    print(n, "s")
    for i in range(100000):
        total += 1
    print(n, "e")

for i in range(3):
    t = threading.Thread(target=test, args=(i,))
    t.start()
    li.append(t)

[t.join() for t in li]
print(total)

相關接口源碼如下(知道代碼所在文件就行):

// ~/Python/sysmodule.c

// 設置線程調度切換基準
static PyObject *
sys_setswitchinterval_impl(PyObject *module, double interval)
/*[clinic end generated code: output=65a19629e5153983 input=561b477134df91d9]*/
{
    if (interval <= 0.0) {
        PyErr_SetString(PyExc_ValueError,
                        "switch interval must be strictly positive");
        return NULL;
    }
    _PyEval_SetSwitchInterval((unsigned long) (1e6 * interval));
    Py_RETURN_NONE;
}

static double
sys_getswitchinterval_impl(PyObject *module)
/*[clinic end generated code: output=a38c277c85b5096d input=bdf9d39c0ebbbb6f]*/
{
    return 1e-6 * _PyEval_GetSwitchInterval();
}

// ~/Python/ceval_gil.h
void _PyEval_SetSwitchInterval(unsigned long microseconds)
{
    _PyRuntime.ceval.gil.interval = microseconds;
}

// 默認5000微秒,即5毫秒
#define DEFAULT_INTERVAL 5000

// 初始化gil時,設置時間片
static void _gil_initialize(struct _gil_runtime_state *gil)
{
    _Py_atomic_int uninitialized = {-1};
    gil->locked = uninitialized;
    gil->interval = DEFAULT_INTERVAL;
}

GC機制

引用計數

python中大部分情況下垃圾回收是靠引用計數來進行管理的,所以在源碼當中會看到大量的增加和減少計數操作,其中對於不同類型的數據,引用計數的邏輯可能也會進行相關的封裝,這裏列舉了最基本的引用計數源碼邏輯:

// 計數+1
static inline void _Py_INCREF(PyObject *op)
{
    _Py_INC_REFTOTAL;
    // 對計數屬性+1
    op->ob_refcnt++;
}

// _PyObject_CAST源碼:
// #define _PyObject_CAST(op) ((PyObject*)(op))
// 就是將對象強轉成PyObject類型
#define Py_INCREF(op) _Py_INCREF(_PyObject_CAST(op))

// 計數-1
static inline void _Py_DECREF(const char *filename, int lineno,
                              PyObject *op)
{
    (void)filename; /* may be unused, shut up -Wunused-parameter */
    (void)lineno; /* may be unused, shut up -Wunused-parameter */
    _Py_DEC_REFTOTAL;
    // 對計數-1,並判斷如果引用爲0,則回收
    if (--op->ob_refcnt != 0) {
// debug下相關操作
#ifdef Py_REF_DEBUG
        if (op->ob_refcnt < 0) {
            _Py_NegativeRefcount(filename, lineno, op);
        }
#endif
    }
    else {
        // 回收操作
        _Py_Dealloc(op);
    }
}

#define Py_DECREF(op) _Py_DECREF(__FILE__, __LINE__, _PyObject_CAST(op))

以後有時間再更新...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章