码农高天
Python程序运行并非不可探知的黑盒,其底层依托Frame上下文维护的栈架构,在C语言死循环中经由Switch分支逐条派发并计算字节码。
AI-assisted summary of 【python】字节码和虚拟机?python代码竟然是这么执行的! — original source. Check the original for full context.
Source published:
Key ideas
- Python 代码在执行前会被编译为 code object,并在运行时由解释器分配的执行帧(frame)中执行,每个函数调用均会生成独立的 frame 来维护局部变量与执行上下文。 (0:00)
- Python 虚拟机采用基于栈(stack-based)的架构,绝大多数字节码均通过压栈、出栈并结合 code object 元数据进行计算与数据传递。 (1:40)
- CPython 的核心字节码执行引擎集中在 Python/ceval.c 的 _PyEval_EvalFrameDefault 函数中,其本质是一个包含巨大 switch 分支的无限循环,且支持被第三方 JIT 工具(如 Numba)重载接管。 (3:45)
- 字节码的执行逻辑与底层硬件汇编语言在原理上完全相通,区别仅在于汇编由物理 CPU 执行,而 Python 字节码由 C 语言实现的软件虚拟机执行。 (5:00)
Detailed notes
## Background 该视频由“码农高天”于2023年10月10日发布在YouTube平台。视频旨在对Python的底层机制进行入门级介绍,重点拆解经常被提及的字节码与虚拟机的基础概念和运行链路。
## TL;DR 视频从Python代码编译生成的code object入手,阐述了执行帧(frame)作为运行环境如何承载局部变量、全局变量和指令序列。接着结合dis模块,以简单的加法函数为例,解释了Python基于栈(stack-based)的字节码工作逻辑。最后深入CPython源码的ceval.c文件,剖析了_PyEval_EvalFrameDefault中庞大的死循环与switch派发机制,揭示了LOAD_FAST、BINARY_ADD等指令在C语言层面的真实操作与退栈流程。
## Atmosphere & style 讲解风格清晰沉稳、循循善诱,既有条分缕析的架构梳理,又有直接对照源码细节的扎实论证。博主语气亲切务实,在剖析庞大的底层C代码时不断宽慰并鼓励读者消除畏难情绪,整体呈现出一种去神秘化、通俗易懂且极具启发性的技术拆解氛围。
## Bulletin - [00:00](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=0s) Python 代码在执行前会被编译为 code object,并在运行时由解释器分配的执行帧(frame)中执行,每个函数调用均会生成独立的 frame 来维护局部变量与执行上下文。 - [01:40](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=100s) Python 虚拟机采用基于栈(stack-based)的架构,绝大多数字节码均通过压栈、出栈并结合 code object 元数据进行计算与数据传递。 - [03:45](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=225s) CPython 的核心字节码执行引擎集中在 Python/ceval.c 的 _PyEval_EvalFrameDefault 函数中,其本质是一个包含巨大 switch 分支的无限循环,且支持被第三方 JIT 工具(如 Numba)重载接管。 - [05:00](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=300s) 字节码的执行逻辑与底层硬件汇编语言在原理上完全相通,区别仅在于汇编由物理 CPU 执行,而 Python 字节码由 C 语言实现的软件虚拟机执行。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=0s) 学习 Python 虚拟机与字节码底层机制有助于在阅读 CPython 源码时建立全局认知,理解程序各模块是如何串联运行的。 - [00:00](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=0s) 所有 Python 代码在运行前都会先被编译成 code object 数据对象。 - [00:00](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=0s) 运行时创建的 frame 提供了局部变量、全局变量和指令序列的环境上下文,并通过记录上一个执行的指令来实现条件分支与跳转。 - [00:00](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=0s) 除了最外层的根 frame 外,每一次函数调用都会在虚拟机中新建一个对应的子 frame。 - [01:40](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=100s) 利用 Python 内置的 dis 模块可以将纯二进制字节码反编译为人类可读的助记符指令(如 LOAD_FAST、BINARY_ADD)。 - [01:40](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=100s) Python 虚拟机基于栈设计,典型加法函数仅需 LOAD_FAST 压栈、BINARY_ADD 计算后回栈、RETURN_VALUE 弹栈返回四步指令。 - [01:40](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=100s) 分析不确定的 Python 底层逻辑时,查阅官方 dis 模块文档或直接反编译查看字节码是最有效的手段之一。 - [03:45](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=225s) CPython 解释字节码的核心实现位于 Python/ceval.c 源码文件中。 - [03:45](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=225s) _PyEval_EvalFrameDefault 是 CPython 中最庞大的核心执行函数,也是默认评估 frame 的入口。 - [03:45](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=225s) CPython 允许重载 _PyEval_EvalFrameDefault,Numba 等 JIT 编译工具正是借此机制介入并替换底层执行流程以实现加速。 - [05:00](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=300s) _PyEval_EvalFrameDefault 内部通过 main_loop 无限循环和 switch(opcode) 进行操作码派发与处理。 - [05:00](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=300s) LOAD_FAST 在 C 源码层面通过 GETLOCAL 读取局部变量、Py_INCREF 增加引用计数并 PUSH 压入栈顶,随后 FAST_DISPATCH 派发下一条指令。 - [05:00](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=300s) RETURN_VALUE 分支会将当前 frame 标记为 FRAME_RETURNED,并通过 goto exiting 打破主死循环退出当前帧。 - [05:00](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=300s) CPython 字节码解释引擎的逻辑模块清晰、贴近直觉,研读其底层源码并不神秘也不深奥。 - [05:00](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=300s) Python 虚拟机的栈操作与状态机循环在设计原理上等同于汇编语言,只是承载计算的实体从物理 CPU 替换为了软件解释器。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=0s) Python 虚拟机与 Frame 的执行环境
在之前的诸多视频中,“bytecode”(字节码)以及“Python 虚拟机”这两个词经常被提及,但一直没有专门讲解虚拟机究竟是如何工作的、其底层机制与工作原理是什么。码农高天强调,做这期视频并不是要让人一下子彻底搞懂 Python 虚拟机的全部细节,而是给大家一个基础概念,建立起全局认知:“这期视频的目的倒不是让你完全理解python的虚拟机,只是为了让你以后再看我的视频的时候,可以知道我在看这块的CPython代码是因为虚拟机是这么连起来的,让你对你的python程序是怎么运行的哎有一个大致的了解。”
所有的 Python 代码在执行前都会被编译成 `code object`。在正式运行代码之前,Python 解释器会建立一个执行帧(`frame`)。借助 Python 标准库的 `inspect` 模块,可以看到许多与 `frame` 相关的信息: - `frame` 为程序提供了一个运行环境,内部包含了代码的局部变量(local variables)、全局变量(global variables)以及该代码对应的字节码。 - `frame` 中还有一个非常关键的信息,即上一个运行的 instruction(上一个执行的 bytecode)。通过在运行时改变它,程序便能完成条件分支或跳转等控制流操作。 - 程序刚启动时会建立初始的根 `frame`;此后,代码中每一次发生函数调用(function call),解释器都会为其新建一个对应的 `frame`。因此,除了最外层的主程序外,每一个 `frame` 都对应着一次函数调用,所有的 bytecode 都是在一个具体的 `frame` 内部被顺序执行的。
```mermaid flowchart TD accTitle: Python Code Execution Setup accDescr: Python code compiles to a code object, which runs within execution frames that hold variables and bytecode instructions. subgraph Compilation["编译阶段"] A["Python 源代码"] --> B["Code Object (代码对象)"] end subgraph Execution["运行时环境 (Frame)"] B --> C["建立 Frame"] C --> D["局部变量与全局变量"] C --> E["Bytecode 指令序列"] C --> F["上一个运行的 Instruction (控制分支)"] end C -->|"函数调用 (Function Call)"| G["新建子 Frame"] ```
[01:40](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=100s) 基于栈(Stack-Based)的字节码与 dis 模块解析
一旦建立了 `frame`,Python 解释器就会开始逐条执行字节码。底层实际存储与运行的字节码是纯二进制数据,但可以通过 Python 自带的 `dis` 模块将其反编译并打印成人类可读的形式,例如展示出 `LOAD_CONST`、`LOAD_NAME`、`CALL_FUNCTION` 等指令。
Python 虚拟机在架构上是典型的“基于栈”(stack-based)设计。简而言之,绝大多数字节码指令要么是在对数据做运算,要么就是在对栈(stack)进行压栈、出栈等操作。以一个仅包含简单加法运算的 `add(a, b)` 函数为例,它被编译后仅有 4 条字节码指令: 1. `LOAD_FAST 0`:将位置为 `0` 的局部变量(即参数 `a` 的值)压入栈中。这里位置参数 `0` 对应参数 `a`,来源于编译期生成的 `code object`。 2. `LOAD_FAST 1`:将位置为 `1` 的局部变量(即参数 `b` 的值)取出来压入栈中。此时栈中包含 `a` 和 `b` 两个数据。 3. `BINARY_ADD`:从栈顶依次弹出这两个数据,计算它们的加和(`a + b`),然后把相加的结果重新压回栈中。此时栈内仅剩一个元素,即计算结果。 4. `RETURN_VALUE`:将当前栈顶的数据弹出并返回给函数的调用者,最终完成了整个函数的返回。
若想了解各个字节码的具体功能,查阅 Python 官方文档中关于 `dis` 模块的说明是最权威的途径。例如: - 文档中对 `LOAD_FAST` 的解释是:将 `co_varnames` 中索引为 `var_num` 的变量压入栈顶。 - 对 `BINARY_ADD` 的描述为实现 `TOS = TOS1 + TOS`。其中 `TOS` 代表 Top of Stack(栈顶),`TOS1` 代表栈顶下方紧邻的元素。 - 对 `RETURN_VALUE` 的定义是将栈顶的数据直接返回给调用该函数的调用者。
码农高天总结道:“所以当你不确定你的Python代码的底层是如何实现的时候呢,把它转成字节码然后再去读它的字节码是一个很有效的方式,这个方式呢我们之前也实践过非常多次了。”
[03:45](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=225s) 深入 CPython 源码:ceval.c 与 _PyEval_EvalFrameDefault
如果觉得官方文档关于字节码的说明仍然不够详尽,想要进一步了解其在 C 语言层面的真实实现,就必须深入 CPython 的源码。在 CPython 源码树中,负责对 `frame` 进行求值(evaluation)的核心逻辑存放在 `Python/ceval.c` 文件中。
该文件中最核心的函数是 `_PyEval_EvalFrameDefault`: - 该函数承担了标准 CPython 执行字节码的任务。函数体极为庞大,“它很有可能是CPython里面最长的函数”。 - CPython 允许用户重载该函数。一些依靠 JIT(即时编译)对 Python 代码进行加速的第三方库(例如 Numba)正是利用了这一机制,通过重载 `_PyEval_EvalFrameDefault` 来介入和接管代码的执行。但在不使用 JIT 扩展的绝大多数日常场景下,CPython 都是在默认运行这个巨大的函数。
[05:00](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=300s) 核心循环机制:Infinite Loop、Switch 派发与退栈流程
在 `_PyEval_EvalFrameDefault` 内部,执行字节码的核心结构是一个主死循环(无限循环)。在源码约 1392 行处,可以看到标有 `main_loop` 标签的无限循环逻辑。在跳过分析和 tracing 代码之后,循环内部实际上是一个巨大的 `switch (opcode)` 语句,它根据当前读取到的操作码(opcode)跳转到对应的分支处理逻辑,构成了运行字节码的中枢。
在这个大 `switch` 中,之前提到的几条加法指令在 C 语言层面的执行逻辑清晰明了: - **`LOAD_FAST`(约 1500 行)**: 1. 通过 `GETLOCAL(oparg)` 根据偏移量变量(即操作数 `0`, `1`, `2` 等)读取对应的局部变量并存入临时变量。 2. 进行错误检查。 3. 执行 `Py_INCREF` 增加对象的引用计数(reference count)。 4. 调用 `PUSH` 宏将该对象指针压入虚拟机栈顶。 5. 执行 `FAST_DISPATCH()`,其语义等价于 `continue`,立即跳入下一次循环去拉取并处理下一条字节码。 - **`BINARY_ADD`**: 1. 从栈顶 `POP` 出一个操作数,再获取新的栈顶元素。 2. 对两者执行加法运算(底层还包含针对字符串拼接等场景的特殊优化)。 3. 通过 `SET_TOP` 将加法得出的结果重置为新的栈顶元素,随后继续派发下一条指令。 - **`RETURN_VALUE`**: 1. 该分支并不以 `FAST_DISPATCH()` 继续循环,而是对当前帧的状态进行标记,将内部状态修改为 `FRAME_RETURNED`,代表当前 `frame` 的生命周期已经结束。 2. 代码末尾通过 `goto exiting` 跳转到退出逻辑。这与此前在讲解 `async` 异步机制时提到的退出路径完全一致,旨在彻底打破外层的无限循环,并将栈顶的最终结果返回给上层调用者。
码农高天宽慰大家在面对底层源码时不要有畏难情绪:“大家想一下这个load fast是不是其实很简单,它没有什么难的啊,就如果你认真的去读的话,你会发现它并不是一个很神秘的东西,它就是一些很简单的事情……所以大家可以看到呢,研究python代码在底层究竟是如何工作的,其实不是一个很困难很复杂的事情,每个人都是有能力来完成这件事的。”
从全局来看,Python 代码的运行闭环可以概括为:每次调用函数或启动程序时生成 `frame`,在 `frame` 构成的上下文中维护一个栈,虚拟机通过无限循环和分支选择逐条执行字节码;字节码负责操作栈或与 `code object` 存储的元数据交互并进行计算,计算结束返回结果后销毁或退出该帧。对于熟悉汇编语言的工程师来说,这一套机制在原理上与底层汇编完全一致,唯一的差别仅在于汇编语言是直接由物理硬件 CPU 执行,而 Python 字节码则是运行在用 C 语言构建的软件虚拟机之上。
## Notable quotes > [01:40](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=100s) 当你不确定你的Python代码的底层是如何实现的时候呢,把它转成字节码然后再去读它的字节码是一个很有效的方式 — 码农高天
> [05:00](https://www.youtube.com/watch?v=FIot_8h5JzQ&t=300s) 研究python代码在底层究竟是如何工作的,其实不是一个很困难很复杂的事情,每个人都是有能力来完成这件事的。 — 码农高天
## Mentioned 码农高天, Python, CPython, Numba, inspect, dis, ceval.c, _PyEval_EvalFrameDefault
## Video description 今天聊一下python的底层机制,对字节码和虚拟机的部分进行一个入门级别的介绍,给大家一个基础概念,因为确实之前的视频中出现过太多次了。
[Source](https://www.youtube.com/watch?v=FIot_8h5JzQ)