> For the complete documentation index, see [llms.txt](https://hai-shi.gitbook.io/cpython-internals/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://hai-shi.gitbook.io/cpython-internals/9-evalutaion-loop/9.3-frame-execution.md).

# 9.3 帧的执行

解析帧的执行过程

## 概述

如前面的编译器和 AST 章节所述，code object 包含要执行的字节码的二进制编码，还包含了变量列表和符号表。

Python 中局部和全局变量的值在运行时才会确定，这些值由运行时函数、模块及代码块的调用方式决定。通过函数 [\_PyEval\_EvalCode()](https://github.com/python/cpython/blob/v3.9.0b1/Python/ceval.c#L4101) 可以将这些变量的值添加到帧中。除此之外，帧还有一些其他的应用方式，例如协程装饰器会动态的生成一个以目标对象为变量的帧。

[PyEval\_EvalFrameEx()](https://github.com/python/cpython/blob/v3.9.0b1/Python/ceval.c#L880) 是一个公共的API，它会调用解释器在 `eval_frame` 属性中配置的帧计算函数。基于 [PEP 523](https://peps.python.org/pep-0523/)，在 Python 3.7 实现了帧计算的可插拔性(提供了 C API，允许使用第三方代码自定义帧的计算函数)。

[\_PyEval\_EvalFrameDefault()](https://github.com/python/cpython/blob/v3.9.0b1/Python/ceval.c#L945) 是 CPython 唯一自带的帧计算默认函数。

这个函数是执行帧的关键，它将所有的东西组合到一起，让代码可以运行起来。同时这个函数经历了数十年的持续优化，因为即便是修改一行代码也会对 CPython 的性能产生巨大的影响。

在 CPython 中，执行任何代码最终都要经过这个帧的求值函数。

{% hint style="info" %}
**注**

当阅读 Python/ceval.c 时，你也许会注意到 C 语言中的宏被十分频繁的使用。C 语言的宏是一种重用代码的方法，同时还减少了函数的调用开销。编译器会将宏直接展开成 C 的代码并编译这些展开后的代码。

在 Linux 或 macOs 上可以使用编译选项 `gcc -E` 查看展开后的代码：

```shell
 $gcc -E Python/ceval.c
```

在 Visual Studio Code 中，可以通过安装 C/C++ 插件去查看这些内联的宏：

<img src="https://1029588898-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FJhewUmzI3BNeGgeFH9Rv%2Fuploads%2Fgit-blob-8a2a955896231f8a15d5f7a88a39e75691f09d48%2F%E5%9B%BE9.3.1%20%E6%9F%A5%E7%9C%8B%E5%86%85%E8%81%94%E5%AE%8F.png?alt=media" alt="图9.3.1 通过插件查看内联宏" data-size="original">

如果使用的是 CLion，选中宏然后按下 `Alt + Space` 就可以查看宏的定义。
{% endhint %}

## 追踪帧的执行过程

通过启用当前线程的跟踪功能，你可以在 Python 3.7 及更高版本中逐步执行每一帧。在 `PyFrameObject` 类型中，有一个类型为 `PyObject *` 名为 `f_trace` 的属性，这个指针指向一个 Python 函数。

下面给出一个代码示例，示例中定义了一个名为 `my_trace()` 的全局追踪函数。该函数可以获取当前帧的栈数据，打印反汇编生成的操作码和一些额外的调试信息。

```python
import sys
import dis
import traceback
import io


def my_trace(frame, event, args):
    frame.f_trace_opcodes = True
    stack = traceback.extract_stack(frame)
    pad = " "*len(stack) + "|"
    if event == 'opcode':
        with io.StringIO() as out:
            dis.disco(frame.f_code, frame.f_lasti, file=out)
            lines = out.getvalue().split('\n')
            [print(f"{pad}{l}") for l in lines]
    elif event == 'call':
         print(f"{pad}Calling {frame.f_code}")
    elif event == 'return':
        print(f"{pad}Returning {args}")
    elif event == 'line':
        print(f"{pad}Changing line to {frame.f_lineno}")
    else:
         print(f"{pad}{frame} ({event} - {args})")
         print(f"{pad}----------------------------------")
    return my_trace

sys.settrace(my_trace)

# Run some code for a demo
eval('"-".join([letter for letter in "hello"])')
```

函数 `sys.settrace()` 将当前线程状态的默认跟踪函数设置成给定的函数。在这之后创建的所有帧都将把 `f_trace` 设置成我们传递的函数。

这段代码将分段打印每个栈中的内容，并在执行下一条字节码前指向它。当帧的计算结果返回时，就会打印 `return` 语句：

![图9.3.2 追踪帧的执行过程](https://1029588898-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FJhewUmzI3BNeGgeFH9Rv%2Fuploads%2Fgit-blob-c620d7ecee69545d70deb29a29b1fee034bbef71%2F%E5%9B%BE9.3.2%20%E8%BF%BD%E8%B8%AA%E5%B8%A7%E7%9A%84%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B.png?alt=media)

你可以在 [`dis`](https://docs.python.org/3/library/dis.html#python-bytecode-instructions) 模块的文档中找到完整的字节码指令集。
