6. Frames Objects

Frame 对象

代码对象包含可执行的字节代码,但缺少执行此类代码所需的上下文信息。 以清单6.0中的一组字节码指令为例,LOAD_COST 将索引作为参数,但是代码对象没有数组或数据结构,该数组或数据结构包含要从其索引处加载值的数据。

代码清单6.0
  1           0 LOAD_CONST               0 (<code object f at 0x000002D7ADE8E540, file "example.py", line 1>)
              2 LOAD_CONST               1 ('f')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (f)
              8 LOAD_CONST               2 (None)
             10 RETURN_VALUE

执行代码对象需要能够提供这种上下文信息的另一种数据结构,这就是框架对象的所在。可以将框架对象视为执行代码对象的容器,并且它引用了某些代码对象执行期间所需的数据和值。 像往常一样,python 确实为我们提供了一些检查 frame 对象的函数,如 sys._getframe() 函数:

代码清单6.1: 访问 frame 对象
>>> import sys
>>> f = sys._getframe()
>>> f
<frame at 0x7fffc07a8ed0, file '<stdin>', line 1, code <module>>
>>> dir(f)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'f_back', 'f_builtins', 'f_code', 'f_globals', 'f_lasti', 'f_lineno', 'f_locals', 'f_trace', 'f_trace_lines', 'f_trace_opcodes']

在可以执行代码对象之前,必须创建一个 frame 对象,在其中执行代码对象。 这样的 frame 对象包含执行代码对象(局部,全局和内置)所需的所有名称空间,对当前执行线程的引用,用于求值字节码的堆栈以及其他对于执行字节码的内部信息。 为了更好地了解 frame 对象,让我们来看一下 Include/frameobject.h 模块中 frame 对象数据结构的定义:

代码清单6.2: frame 对象定义
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;

结构体定义中的字段和文档并不难理解,但我们会提供有关这些字段以及它们与字节码执行之间的关系的更多详细信息:

  1. f_back:该字段是对在当前代码对象之前执行的代码对象所在 frame 的引用。 给定一组 frame 对象,这些 frame 的 f_back 字段一起组成一个 frame 栈结构,这些 frame 一直返回到初始 frame。 然后,初始 frame 在 f_back 字段的值为 NULL 。 这种隐式 frame 堆栈形成了我们称为调用堆栈的 frame。

  2. f_code:该字段是对代码对象的引用。 此代码对象包含在该 frame 的上下文中执行的字节码。

  3. f_builtins:这是对内置名称空间的引用。 该名称空间包含诸如 printenumerate 等名称及其对应的值。

  4. f_globals:这是对代码对象的全局名称空间的引用。

  5. f_locals:这是对代码对象的局部名称空间的引用。 如前所述,这些名称已在函数范围内定义。 当我们讨论 f_localplus 字段时,我们将看到 python 在使用局部定义的名称时所做的优化。

  6. f_valuestack:这是对 frame 求值堆栈的引用。 回想一下,Python 虚拟机是基于堆栈的虚拟机,因此在对字节码进行求值期间,将从堆栈的顶部读取值,并将字节码求值的结果存储在堆栈的顶部。 该字段是在代码对象执行期间使用的堆栈。 frame 代码对象的堆栈大小提供了此数据结构可以扩展到的最大深度。

  7. f_stacktop:顾名思义,该字段指向求值堆栈的下一个空闲插槽。 新创建框架时,此值设置为值堆栈,这是堆栈上的第一个可用空间,因为堆栈上没有任何项目。

  8. f_trace:该字段引用了一个用于跟踪 python 代码执行情况的函数。

  9. f_exc_typef_exc_valuef_exc_tracebackf_gen:是用于执行生成器代码的字段。 当我们讨论 python 生成器时,将对此进行更多介绍。

  10. f_local_plus:这是对包含足够用于存储单元格和局部变量的空间的数组的引用。 该字段为求值循环提供了一种机制,该机制可使用 LOAD_FASTSTORE_FAST 指令来优化名称与值堆栈之间的加载和存储。 LOAD_FASTSTORE_FAST 操作码提供了比其对应的LOAD_NAMESTORE_NAME 操作码更快的名称访问权限,因为它们使用数组索引来访问名称值,并且此操作大约在恒定的时间内完成,这与它们的对应程序在映射中查找与给定名称关联的值不同。 当我们讨论求值循环时,我们将看到在 frame 自举过程中如何设置此值。

  11. f_blockstack:该字段引用充当堆栈的数据结构,该数据结构用于处理循环和异常处理。 这是除了对虚拟机最重要的值堆栈之外的第二个堆栈,但它没有得到应有的重视。 块堆栈,异常和循环结构之间的关系非常复杂,我们将在接下来的章节中进行介绍。

6.1 分配 Frame 对象

框架对象在 python 代码求值期间无处不在,执行的每个代码块都需要一个提供一些上下文的框架对象。 通过调用 Objects/frameobject.c 模块中的 PyFrame_New 函数来创建新的框架对象。 这个函数被调用了很多次,每当执行代码对象时,就会使用两个主要的优化方法来减少调用该函数的开销,我们将简要介绍这些优化方法。

首先,代码对象具有一个字段 co_zombieframe,该字段引用了一个惰性 frame 对象。 当执行一个代码对象时,执行它的 frame 不会立即被释放。 将该 frame 保留在 co_zombieframe 中,因此当下一个执行相同的代码对象时,无需花费时间为新的执行帧分配内存。 ob_typeob_sizef_codef_valuestack字段保留其值; f_localsf_tracef_exc_typef_exc_valuef_exc_tracebackNULLf_localplus 保留其分配的空间,但局部变量为空。 其余字段不包含对任何对象的引用。 虚拟机使用的第二个优化是维护预分配 frame 对象的 freelist,从中可以获取帧以执行代码对象。

Frame 对象的源代码实际比较通俗易懂的,通过查看在封闭的代码对象执行后如何分配分配的 frame,可以看到 zombie framefreelist 的概念是如何实现的。 代码清单6.3显示了帧重新分配的有趣部分。

代码清单6.3: 回收 frame 对象
    if (co->co_zombieframe == NULL)
        co->co_zombieframe = f;
    else if (numfree < PyFrame_MAXFREELIST) {
        ++numfree;
        f->f_back = free_list;
        free_list = f;
    }
    else
        PyObject_GC_Del(f);

仔细观察会发现,只有在进行递归调用(即代码对象试图执行自身)时,freelist 才会增长,因为这是 zombieframe 字段唯一为 NULL 的时间。 使用 freelist 的这种微小优化有助于在某种程度上消除此类递归调用的重复内存分配。

本章不涉及 frame 对象紧密结合的求值循环,而涵盖 frame 对象的要点。 讨论中仍然遗漏了一些内容,但我们将在后续章节中进行介绍。 例如:

  1. 当代码执行进行到 return 语句时,值如何从一个 frame 传递到下一个 frame?

  2. 线程状态是什么,线程状态从何而来?

  3. 当在执行 frame 中引发异常时,异常如何使框架堆栈中的 bubble 消失? 等等

当我们在下一章中介绍非常重要的解释器和线程状态数据结构,然后在后续各章中讨论求值循环时,将回答大多数这些问题。

Last updated