# Python程序执行过程

# Python程序执行过程

  • Python 程序执行原理本质上跟 Java 或者 C# 一样,都可以归纳为 虚拟机字节码 。 Python 执行程序分为两步:先将程序代码编译成字节码,然后启动虚拟机逐行执行编译器生成的字节码

    image

  • Python是一种解释性语言,但并不意味着Python程序不用编译,只是解释器同时完成了编译和运行两个动作而已-与Java相比,需要自己先编译成JAVAC字节码,然后再运行字节码文件

# 编译

# PyCodeObject

  • 可以使用compile​​ 内置函数编译程序成一个对应的PyCodeObject​​对象用于存储编译结果。

    compile(text, 'demo.py', 'exec')
    # source ,待编译 源码 ;
    # filename ,源码所在 文件名 ;
    # mode , 编译模式 
    	# exec 表示将源码当做一个模块来编译;
    	# single ,用于编译一个单独的 Python 语句(交互式下);
    	# eval ,用于编译一个 eval 表达式;
    
    Help on built-in function compile in module builtins:
    
    compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1)
        Compile source into a code object that can be executed by exec() or eval().
    
        The source code may represent a Python module, statement or expression.
        The filename will be used for run-time error messages.
        The mode must be 'exec' to compile a module, 'single' to compile a
        single (interactive) statement, or 'eval' to compile an expression.
        The flags argument, if present, controls which future statements influence
        the compilation of the code.
        The dont_inherit argument, if true, stops the compilation inheriting
        the effects of any future statements in effect in the code calling
        compile; if absent or false these statements do influence the compilation,
        in addition to any features explicitly specified.
    
  • PyCodeObject​是Python内键对象

  • Python将源码编译后,每个一个作用域会对应着一个PyCodeObject​对象,子作用域代码对象位于父作用域代码对象的常量列表中,因此会经常呈现嵌套的模式。

    image

  • 同时Python会将编译后的结果保存在pyc文件中,后续Python导入相应模块时只需要读取pyc文件并反序列化即可,避免了重复编译导致的开销。

    • 文件有修改时会重新编译
    • 只有被import​的包才会被缓存,所以入口文件一般是没有pyc文件的
    • 实质上可以类比java中的.java文件,只不过 Java 程序需要先用编译器 javac 命令来编译,再用虚拟机 java 命令来执行;而 Python 解释器把这个两个活都干了,更加智能。

# 反编译

  • 可以使用dis模块将代码对象的字节码编译成Python虚拟机执行的“汇编语言”

    • 格式:偏移量+指令+操作数
    • 以语句为单位进行分组,中间以空行隔开,语句行号在字节码前给出

    image

    >>> dis.dis(code)
      1           0 LOAD_CONST               0 (<code object main at 0x0000013BE4AF80C0, file "", line 1>)
                  2 LOAD_CONST               1 ('main')
                  4 MAKE_FUNCTION            0
                  6 STORE_NAME               0 (main)
    
      4           8 LOAD_NAME                1 (__name__)
                 10 LOAD_CONST               2 ('__main__')
                 12 COMPARE_OP               2 (==)
                 14 POP_JUMP_IF_FALSE       22
    
      5          16 LOAD_NAME                0 (main)
                 18 CALL_FUNCTION            0
                 20 POP_TOP
            >>   22 LOAD_CONST               3 (None)
                 24 RETURN_VALUE
    
    Disassembly of <code object main at 0x0000013BE4AF80C0, file "", line 1>:
      2           0 LOAD_GLOBAL              0 (print)
                  2 LOAD_CONST               1 ('hello world')
                  4 CALL_FUNCTION            1
                  6 POP_TOP
                  8 LOAD_CONST               0 (None)
                 10 RETURN_VALUE
    >>> print(code.co_consts)
    (<code object main at 0x0000013BE4AF80C0, file "", line 1>, 'main', '__main__', None)
    >>> print(code.co_names)
    ('main', '__name__')
    

# 执行

# PyFrameObject

  • 由于代码对象PyCodeObject​​是静态的,因此虚拟机需要一个辅助对象来维护执行上下文,这就是PyFrameObject​​,其至少需要满足以下要求

    • 利用一个动态容器来存储对象作用域中的局部名字空间,以及全局名字空间和内建名字空间的具体空间,以此保证相关名字查找顺利。
    • 需要保存当前执行的字节码指令的编码-就像CPU需要一个寄存器(IP)保存当前执行指令位置一样

    image

  • PyFrameObject​​定义如下:

    typedef struct _frame {
        PyObject_VAR_HEAD
        struct _frame *f_back;      /* 前一个栈帧对象,也就是调用者 previous frame, or NULL */
        PyCodeObject *f_code;       /* 代码的字节码对象 code segment */
        PyObject *f_builtins;       /* 内建名字空间 builtin symbol table (PyDictObject) */
        PyObject *f_globals;        /* 全局名字空间 global symbol table (PyDictObject) */
        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 */
        PyObject *f_localsplus[1];  /* 静态存储的局部名字空间和临时栈 locals+stack, dynamically sized */
    } PyFrameObject;
    
  • 注意到f_back​指向前一个栈帧对象-调用者的栈帧对象,以此按照调用关系能够串成一个调用链,例如我们有demo.py

    pi = 3.14
    
    def square(r):
        return r ** 2
    
    def circle_area(r):
        return pi * square(r)
    
    def main():
        print(circle_area(5))
      
    if __name__ == '__main__':
        main()
    

    则其调用链如下:

    image

  • 可以使用sys._getframe​获取当前栈帧对象

    import sys
    def fun(r):
        frame = sys._getframe()
        while frame:
            print('#', frame.f_code.co_name)
            print('Locals:', list(frame.f_locals.keys()))
            print('Globals:', list(frame.f_globals.keys()))
            print()
    
            frame = frame.f_back
    
    # 实现自己的getframe函数
    def getframe():
        try:
            1 / 0
        except Exception as e:
            return e.__traceback__.tb_frame.f_back
    
  • 栈帧对象将贯彻代码对象执行的始终,负责维护执行时所需的一切上下文信息。

# 字节码执行

  • Python 虚拟机执行代码对象的代码位于 Python/ceval.c 中,主要函数有两个: PyEval_EvalCodeEx 是通用接口,一般用于函数这样带参数的执行场景; PyEval_EvalCode 是更高层封装,用于模块等无参数的执行场景。这两个函数最终调用_PyEval_EvalCodeWithName函数,初始化栈帧对象并调用PyEval_EvalFrame系列函数进行处理,PyEval_EvalFrame函数最终调用_PyEval_EvalFrameDefault函数。

    image

    PyObject *
    PyEval_EvalCode(PyObject *co, PyObject *globals, PyObject *locals);
    
    PyObject *
    PyEval_EvalCodeEx(PyObject *_co, PyObject *globals, PyObject *locals,
                      PyObject *const *args, int argcount,
                      PyObject *const *kws, int kwcount,
                      PyObject *const *defs, int defcount,
                      PyObject *kwdefs, PyObject *closure);
    
    PyObject *
    PyEval_EvalFrame(PyFrameObject *f);
    
    PyObject *
    PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
    
    PyObject* _Py_HOT_FUNCTION
    _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag);
    
  • _PyEval_EvalFrameDefault最终负责字节码的执行,其大致逻辑如下

    PyObject* _Py_HOT_FUNCTION
    _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
    {
        // 逐条取出字节码来执行
        for (;;) {
            // 读取下条字节码
            // 字节码位于: f->f_code->co_code, 偏移量由 f->f_lasti 决定
            opcode, oparg = read_next_byte_code(f);
            if (opcode == NULL) {
                break;
            }
    
            switch (opcode) {
                // 加载常量
                case LOAD_CONST:
                    // ....
                    break;
                // 加载名字
                case LOAD_NAME:
                    // ...
                    break;
                // ...
            }
        }
    }
    

# 顺序

有例:

pi = 3.14
r = 3
area = pi * r ** 2

其字节码反编译结果为:

  1           0 LOAD_CONST               0 (3.14)
              2 STORE_NAME               0 (pi)

  2           4 LOAD_CONST               1 (3)
              6 STORE_NAME               1 (r)

  3           8 LOAD_NAME                0 (pi)
             10 LOAD_NAME                1 (r)
             12 LOAD_CONST               2 (2)
             14 BINARY_POWER
             16 BINARY_MULTIPLY
             18 STORE_NAME               2 (area)
             20 LOAD_CONST               3 (None)
             22 RETURN_VALUE

其虚拟机变化情况如下:

  1. 初始状态

    image

  2. 执行LOAD_CONST 0​命令将操作数为下标的常量从常量表中加载出来并压入栈帧对象尾部的临时栈

    image

  3. 执行STORE_NAME 0​ 命令将栈帧对象临时栈的栈顶元素弹出并保存到局部名字空间,名字下标由操作数指定

    不直接放入局部名字空间的原因是Python字节码只有一个操作数,因此复杂操作需要多条指令组合完成,类似于CPU精简指令集。

    image​​

# if判断

有例:

value = 1
if value < 0:
    print('negative')
else:
    print('positive')

其字节码反编译如下:

  1           0 LOAD_CONST               0 (1)
              2 STORE_NAME               0 (value)

  2           4 LOAD_NAME                0 (value)
              6 LOAD_CONST               1 (0)
              8 COMPARE_OP               0 (<)
             10 POP_JUMP_IF_FALSE       22

  3          12 LOAD_NAME                1 (print)
             14 LOAD_CONST               2 ('negative')
             16 CALL_FUNCTION            1
             18 POP_TOP
             20 JUMP_FORWARD             8 (to 30)

  4     >>   22 LOAD_NAME                1 (print)
             24 LOAD_CONST               3 ('positive')
             26 CALL_FUNCTION            1
             28 POP_TOP
        >>   30 LOAD_CONST               4 (None)
             32 RETURN_VALUE

image

image

# while循环

有例:

values = [1, 2, 3]

while values:
    print(values.pop())

其字节码反编译如下:

  1           0 LOAD_CONST               0 (1)
              2 LOAD_CONST               1 (2)
              4 LOAD_CONST               2 (3)
              6 BUILD_LIST               3
              8 STORE_NAME               0 (values)

  3          10 SETUP_LOOP              20 (to 32)
        >>   12 LOAD_NAME                0 (values)
             14 POP_JUMP_IF_FALSE       30

  4          16 LOAD_NAME                1 (print)
             18 LOAD_NAME                0 (values)
             20 LOAD_METHOD              2 (pop)
             22 CALL_METHOD              0
             24 CALL_FUNCTION            1
             26 POP_TOP
             28 JUMP_ABSOLUTE           12
        >>   30 POP_BLOCK
        >>   32 LOAD_CONST               3 (None)
             34 RETURN_VALUE

image