一条Python语句可能会被拆分成多条字节码指令,一组Python语句构成一个Code_Object ,在Python的编译阶段(Python和编译和解释是一起的,但方便讨论,我们暂且分开看),会把所有运行时所需的字节码准备完毕,存在各个Code_Object中,并在内部有C的指针指向这些Code_Object,当需要执行的时候,Code_Object里的字节码就会被执行。

值得注意的是,某个Code_Object可能会在一次函数运行时被执行很多次,Python需要一套体系用来管理各个指令的执行,这里就涉及到Frame。

Python的Frame其实是提供了一套字节码的管理功能,实现了对字节码的查询、管理、加载、释放等

一个递归函数的例子(思考foo的code_object被执行了多少次?)

# File : frame.py

def foo(x):
  if x<1:
    return x
  else:
    return foo(x-1)

foo(1)

首先python -m dis frame.py 查看函数的汇编结果,第一部分是主框架的字节码,第二部分是函数code_object的汇编结果。函数的执行是在运行时候定义的,所以递归调用在这个时候不会展开,在调用的地方,只是存储了待调用函数的函数名。

  1           0 LOAD_CONST               0 (<code object foo at 0x1045b9030, file "frame.py", line 1>)
              2 LOAD_CONST               1 ('foo')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (foo)

  7           8 LOAD_NAME                0 (foo)
             10 LOAD_CONST               2 (1)
             12 CALL_FUNCTION            1
             14 POP_TOP
             16 LOAD_CONST               3 (None)
             18 RETURN_VALUE

Disassembly of <code object foo at 0x1045b9030, file "frame.py", line 1>:
  2           0 LOAD_FAST                0 (x)
              2 LOAD_CONST               1 (1)
              4 COMPARE_OP               0 (<)
              6 POP_JUMP_IF_FALSE       12

  3           8 LOAD_FAST                0 (x)
             10 RETURN_VALUE

  5     >>   12 LOAD_GLOBAL              0 (foo)
             14 LOAD_FAST                0 (x)
             16 LOAD_CONST               1 (1)
             18 BINARY_SUBTRACT
             20 CALL_FUNCTION            1
             22 RETURN_VALUE
             24 LOAD_CONST               0 (None)

我们先用一张图解释Python的Frame如何在这次的函数调用中,动态的管理Code_Object的,给自己一个体感,具体的代码分析放到最后。

Step1:

首先,当汇编指令执行到7-12行 CALL_FUNCTION时,整个Python虚拟机内部状态如图:

在这一步时,函数已经构建完毕了,正如前两章所述,我们把函数的function_object展开,里面存了一个code_object,再展开,code_object里有一个bytecode_object,这就是编译后字节码。而字节码中有一行call_function的指令,对应的Python代码的递归调用。 当字节码指令执行到这一行时,会从hash表里找到函数名对应的object,进一步执行。

Step2:

这时,我们已经深入函数内部,开始执行函数内部的code_object了。
Python在这一步,会创建一个新的FrameObject,这个FrameObject包含了我们将要执行的新的字节码(byte_code),同时,还有一个指针指向上一步的Frame,以及一个offset存储了当前的指令偏移地址。

Step3:

类似的,如果递归的退出条件没有满足,新的Frame将一直被创建

Step4:

当一个Frame B的所有字节码全部执行完毕,将执行退出Frame的操作。当前的Frame B里存储了创建他的Frame A的指针,Python会借助该指针定位到Frame A,而我们已经提过,Frame A中是有指令偏移地址offset的,因此,Python会从上次frame跳转的地方继续执行字节码。而如果frame为空,则意味着程序运行结束了。

FrameObject 管理了什么

上文说到,Python用FrameObject实现了对字节码运行的管理,那Frame到底管理了什么,我们通过代码frameobject.h来查看。

// File : cpython/Include/frameobject.h
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 */
    // ...
    int f_lasti;                /* Last instruction if called */
    char f_executing;           /* whether the frame is still executing */
    // ...
} PyFrameObject;

总览一下,frameobject主要存了几个重要信息:

  • 运行的上下文信息,f_back存储了上一个frame的指针,用于当前frame运行结束后的返回,f_code存储了CodeObject的指针,用来找到当前frame运行的字节码
  • 变量表,包括符号f_buildins、当前的变量f_locals和全局变量f_globals
  • value stack,哈!我们之前说的value stack,原来是frameobject通过**f_valuestack维护的
  • 运行状态信息,比如上次执行的指令f_lasti和当前是否在执行的标记f_executing

Python在多个CodeObject间运行和切换的过程,其实就是frameobject的创建和销毁的过程。

追溯一次frame的创建过程

如果想以函数为例,追溯frame的创建过程,可以依次追溯以下函数调用追溯到:

  • TARGET(CALL_FUNCTION) 条件 (cpython/Python/ceval.c)
  • call_function函数 (cpython/Python/ceval.c)
  • _PyFunction_FastCallKeywords函数 (cpython/Objects/call.c)
  • function_code_fastcall函数(cpython/Objects/call.c)
  • _PyFrame_New_NoTrack 你会看到这个函数,这就是新建的Frame,在它的下方有个PyEval_EvalFrameEx,就是执行这个frame

results matching ""

    No results matching ""