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 ( at 0x000001CD2FEA4930, file "xxx.py", line 6>)
              6 LOAD_CONST               3 ('test..')
              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
# 
函数本质

当创建函数时,函数对象就会绑定相关的执行接口_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 ()
             22 LOAD_CONST               5 ('test..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))

#  
# 
# 
#  
#  <__main__.A object at 0x00000186AE62D198>
# 
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__)

#  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

# 查找到的是builtin里的list
>>> list = 1
# 在local定义list(全局区的local和global指向的是同一个)
>>> list
1
# 获取的是local的list
>>> del list
# 删除的是local的list
>>> list

# 可以看到再次获取到了builtin中的list
>>> __builtins__.list

>>> __builtins__.list = 1
# 修改builtin中的list,会导致很多内部对list的使用出问题,程序崩溃
>>> list
1
Exception ignored in: 
...
命名操作规则

对命名进行的操作如果会影响命名空间发生改变,那么操作将作用于局部作用域,例如赋值操作、删除操作等,举例:

>>> globals() is 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))
标记清除
gc管理对象

标记清除只会对gc管理的对象类型进行标记和监控,然后将其中不可达的对象清除。而对于gc管理的类型,会在定义时,在tp_flags属性中进行设置,例如list类型的tp_flags定义如下:

Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_LIST_SUBCLASS
// Py_TPFLAGS_HAVE_GC就是gc管理对象的定义,宏定义如下:
// #define Py_TPFLAGS_HAVE_GC (1UL << 14)

可以通过类对象的__flags__属性查看,第15位如果为1,则为gc管理对象,下面封装了函数来判断是否为gc管理对象:

def is_gc_type(t):
    return bin(t.__flags__)[-15] == "1"

print(is_gc_type(int))
print(is_gc_type(float))
print(is_gc_type(bool))
print(is_gc_type(str))
print(is_gc_type(list))
print(is_gc_type(set))
print(is_gc_type(dict))

# False
# False
# False
# False
# True
# True
# True

而对象gc管理的对象类型,都会通过PyObject_GC_New方法来创建对象,例如dict对象的创建源码:

// 创建一个dict对象
static PyObject *
new_dict(PyDictKeysObject *keys, PyObject **values)
{
    PyDictObject *mp;
    assert(keys != NULL);
    // 缓存池操作
    if (numfree) {
        // ...
    }
    else {
        // 通过PyObject_GC_New创建gc管理的对象
        mp = PyObject_GC_New(PyDictObject, &PyDict_Type);
        // ...
    }
    // ...
}

并且使用PyObject_GC_New创建的对象,则会被添加到gc第0代的链表当中,源码如下:

#define PyObject_GC_New(type, typeobj) \
                ( (type *) _PyObject_GC_New(typeobj) )

PyObject *
_PyObject_GC_New(PyTypeObject *tp)
{
    PyObject *op = _PyObject_GC_Malloc(_PyObject_SIZE(tp));
    if (op != NULL)
        op = PyObject_INIT(op, tp);
    return op;
}

PyObject *
_PyObject_GC_Malloc(size_t basicsize)
{
    return _PyObject_GC_Alloc(0, basicsize);
}

// 创建一个gc管理对象的实现
static PyObject *
_PyObject_GC_Alloc(int use_calloc, size_t basicsize)
{
    struct _gc_runtime_state *state = &_PyRuntime.gc;
    PyObject *op;
    PyGC_Head *g;
    size_t size;
    if (basicsize > PY_SSIZE_T_MAX - sizeof(PyGC_Head))
        return PyErr_NoMemory();
    size = sizeof(PyGC_Head) + basicsize;
    // 分配创建对象的内存
    if (use_calloc)
        g = (PyGC_Head *)PyObject_Calloc(1, size);
    else
        g = (PyGC_Head *)PyObject_Malloc(size);
    if (g == NULL)
        return PyErr_NoMemory();
    assert(((uintptr_t)g & 3) == 0);  // g must be aligned 4bytes boundary
    g->_gc_next = 0;
    g->_gc_prev = 0;
    // 第0代数量+1
    state->generations[0].count++; /* number of allocated GC objects */
    // 如果达到第0代回收条件则进行回收
    if (state->generations[0].count > state->generations[0].threshold &&
        state->enabled &&
        state->generations[0].threshold &&
        !state->collecting &&
        !PyErr_Occurred()) {
        state->collecting = 1;
        collect_generations(state);
        state->collecting = 0;
    }
    // 添加到gc管理的链表
    op = FROM_GC(g);
    return op;
}

当进行标记清除时,则会遍历每一代的头结点,将不可达的进行回收

自定义gc对象类型

在创建类对象时,type会根据类的尺寸、继承的基类等来判断是否为需要gc管理的类型,例如将类的__slots__属性设为空元组,那么创建的对象就不会有自定义的属性存在,所以也就不用gc来管理了(gc只管理那些可能造成循环引用的对象),源码如下:

type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds)
{
    // ...
    // 如果父类是gc管理的,或者当前类的尺寸比父类大(还有父类以外的属性),则需要进行gc管理
    if ((base->tp_flags & Py_TPFLAGS_HAVE_GC) ||
        type->tp_basicsize > base->tp_basicsize)
        type->tp_flags |= Py_TPFLAGS_HAVE_GC;
    // ...
}

type创建对象时,也会结合是否为gc管理对象来选择创建对象的方式,源码如下:

type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds)
{
    // ...
    // type创建对象的方法
    type->tp_alloc = PyType_GenericAlloc;
    // ...
}

PyObject *
PyType_GenericAlloc(PyTypeObject *type, Py_ssize_t nitems)
{
    PyObject *obj;
    const size_t size = _PyObject_VAR_SIZE(type, nitems+1);
    // 对于gc管理的对象则用对应的方式创建对象
    if (PyType_IS_GC(type))
        obj = _PyObject_GC_Malloc(size);
    else
        obj = (PyObject *)PyObject_MALLOC(size);
    // ...
    return obj;
}

示例如下:

def is_gc_type(t):
    return bin(t.__flags__)[-15] == "1"

class A:
    pass

class B:
    __slots__ = ()

class C(A):
    __slots__ = ()

print(is_gc_type(A))
print(is_gc_type(B))
print(is_gc_type(C))

# True
# False
# True
分代回收

从前面的标记清除可以发现:当创建gc管理的对象时,会进行分代回收阈值条件的判断,当符合条件时,就会自动进行垃圾回收,源码如下:

// 分代回收逻辑
static Py_ssize_t
collect_generations(struct _gc_runtime_state *state)
{
    /* Find the oldest generation (highest numbered) where the count
     * exceeds the threshold.  Objects in the that generation and
     * generations younger than it will be collected. */
    Py_ssize_t n = 0;
    for (int i = NUM_GENERATIONS-1; i >= 0; i--) {
        // 当前代的计数大于设置阈值(第一代的计数和二三代的意思不同),才能进行回收操作
        if (state->generations[i].count > state->generations[i].threshold) {
            /* Avoid quadratic performance degradation in number
               of tracked objects. See comments at the beginning
               of this file, and issue #4074.
            */
            if (i == NUM_GENERATIONS - 1
                && state->long_lived_pending < state->long_lived_total / 4)
                continue;
            // 调用回收函数,并返回回收的对象数量
            n = collect_with_callback(state, i);
            // 只会对第一个符合情况的代进行回收
            break;
        }
    }
    return n;
}

// 垃圾回收函数
static Py_ssize_t
collect_with_callback(struct _gc_runtime_state *state, int generation)
{
    assert(!PyErr_Occurred());
    Py_ssize_t result, collected, uncollectable;
    // 垃圾回收开始的回调
    invoke_gc_callback(state, "start", generation, 0, 0);
    // 真正回收的实现
    result = collect(state, generation, &collected, &uncollectable, 0);
    // 垃圾回收结束回调
    invoke_gc_callback(state, "stop", generation, collected, uncollectable);
    assert(!PyErr_Occurred());
    return result;
}

// 回收主逻辑
static Py_ssize_t
collect(struct _gc_runtime_state *state, int generation,
        Py_ssize_t *n_collected, Py_ssize_t *n_uncollectable, int nofail)
{
    // ...

    // 下一代的计数+1
    if (generation+1 < NUM_GENERATIONS)
        state->generations[generation+1].count += 1;
    // 清空当前代及更年轻代的计数
    for (i = 0; i <= generation; i++)
        state->generations[i].count = 0;

    // ...

    // 寻找和删除不可达对象
    gc_list_init(&unreachable);
    move_unreachable(young, &unreachable);

    // ...

    // 统计并更新回收、未回收对象的数量
    if (n_collected) {
        *n_collected = m;
    }
    if (n_uncollectable) {
        *n_uncollectable = n;
    }

    struct gc_generation_stats *stats = &state->generation_stats[generation];
    stats->collections++;
    stats->collected += m;
    stats->uncollectable += n;

    // ...

    return n+m;
}

或者我们也可以通过gc.collect接口来手动调用垃圾回收,源码如下:

/* API to invoke gc.collect() from C */
// 垃圾回收接口
Py_ssize_t
PyGC_Collect(void)
{
    struct _gc_runtime_state *state = &_PyRuntime.gc;
    if (!state->enabled) {
        return 0;
    }

    Py_ssize_t n;
    if (state->collecting) {
        /* already collecting, don't do anything */
        n = 0;
    }
    else {
        PyObject *exc, *value, *tb;
        // 设置开始回收标志
        state->collecting = 1;
        PyErr_Fetch(&exc, &value, &tb);
        // 调用垃圾回收
        n = collect_with_callback(state, NUM_GENERATIONS - 1);
        PyErr_Restore(exc, value, tb);
        state->collecting = 0;
    }

    return n;
}

手动调用垃圾回收示例如下:

import gc

# 先清空一次,避免影响结果
gc.collect()

# 添加gc回调
gc.callbacks.append(lambda flag, status: print(flag, status))
a = [None]
b = [a]
a[0] = b

print("start del...")
del a, b
print("end del...")
# 这里没有达到分代回收的阈值,所以手动调用垃圾回收
print("collect num:", gc.collect())
print("end")

# start del...
# end del...
# start {'generation': 2, 'collected': 0, 'uncollectable': 0}
# stop {'generation': 2, 'collected': 2, 'uncollectable': 0}
# collect num: 2
# end
# start {'generation': 2, 'collected': 0, 'uncollectable': 0}
# stop {'generation': 2, 'collected': 0, 'uncollectable': 0}

待更新...

你可能感兴趣的:(Python 源码分析-运行机制)