文章目录
- [1 `code object`和`frame`](#1
code object
和frame
) - [2 栈虚拟机](#2 栈虚拟机)
- [3 CPython源码中的实现](#3 CPython源码中的实现)
- [4 总结](#4 总结)
1 code object
和frame
Python代码在执行前,并非直接被CPU理解和运行。它经历了一个编译和解释的过程。
-
编译成code object:
编写的所有Python代码,无论是脚本文件还是一个函数,首先都会被编译器(Compiler)转换成一种中间形式,称为字节码(Bytecode)。这些字节码被打包存储在一个名为code object的结构中。这个对象不仅包含了字节码指令,还包含了执行这段代码所需的各种元信息,如常量、变量名等。
-
创建执行环境frame:
当Python解释器准备执行一段字节码时,它会首先创建一个帧(frame)。可以把frame理解为一个独立的、用于执行代码的"沙盒"或上下文环境。
- 何时创建 :
- 程序启动时会创建一个顶层
frame
。 - 每当调用一个函数时,都会为该函数的执行创建一个新的
frame
。
- 程序启动时会创建一个顶层
frame
包含什么 :- 局部变量(f_locals):函数内部定义的变量。
- 全局变量(f_globals):在全局作用域中可访问的变量。
- 代码对象引用(f_code) :指向需要在此
frame
中执行的那个code object
。 - 指令指针(f_lasti):记录上一条执行过的字节码指令位置,控制代码的分支和流程。
- 一个数据栈(f_back):这是虚拟机执行字节码操作的核心区域,我们稍后会详细介绍。
- 何时创建 :
2 栈虚拟机
Python虚拟机的一个核心设计思想是**"基于栈"(Stack-Based)**。这意味着几乎所有的字节码指令都在做两类事情之一:
- 操作数据栈:将数据(如常量、变量的值)压入栈顶(Push),或者从栈顶弹出数据(Pop)。
- 进行计算:从栈顶取出(Pop)一个或多个数据,进行计算(如加法、比较),然后将计算结果再压回(Push)栈顶。
这个"栈"是一个后进先出(LIFO, Last-In, First-Out)的数据结构,就像一摞盘子,你最后放上去的盘子总是最先被取走。
实例:一个加法函数
假设有这样一个简单的Python函数:
Python
def add(a, b):
return a + b
使用dis
模块(disassembler,反汇编器)可以查看它对应的字节码:
2 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_ADD
6 RETURN_VALUE

下面我们来逐步分解虚拟机的执行流程:
LOAD_FAST 0 (a)
LOAD_FAST
是一条加载局部变量的指令。0
是变量a
在code object
变量名列表中的索引。- 动作 :虚拟机找到局部变量
a
的值,并将其压入(Push)栈顶 。此时,栈里只有一个元素:a
的值。
LOAD_FAST 1 (b)
1
是变量b
的索引。- 动作 :虚拟机找到局部变量
b
的值,并将其压入(Push)栈顶 。现在,栈从底到顶依次是:a
的值,b
的值。
BINARY_ADD
- 这是一条二元加法指令。
- 动作 :
- 从栈顶**弹出(Pop)**一个元素,得到
b
的值。 - 再次从栈顶**弹出(Pop)**一个元素,得到
a
的值。 - 计算这两个值的和(a + b)。
- 将计算出的结果压回(Push)栈顶。
- 从栈顶**弹出(Pop)**一个元素,得到
- 执行完毕后,栈里只有一个元素:
a + b
的和。
RETURN_VALUE
- 这是一条函数返回指令。
- 动作 :从栈顶**弹出(Pop)**唯一的元素(即
a + b
的和),并将其作为整个函数的返回值。这个frame
的生命周期至此结束。
3 CPython源码中的实现
如果我们想知道这些字节码指令究竟是如何实现的,就需要查看CPython(官方的Python实现)的C语言源代码。
- 核心文件 :
ceval.c
- 核心函数 :
_PyEval_EvalFrameDefault
这个函数是CPython的"心脏",它负责解释和执行字节码。其内部结构大致如下:
-
一个巨大的主循环(main_loop) :这个循环会不断地读取并执行
code object
中的下一条字节码指令,直到没有指令可执行。 -
一个庞大的
switch
语句 :在循环内部,有一个switch
语句,它根据当前读取到的字节码指令类型(Opcode),跳转到相应的C代码块去执行。
例如:
LOAD_FAST
的实现 :对应的C代码会通过GETLOCAL(oparg)
获取局部变量,然后通过PUSH()
宏将其压入虚拟机栈。BINARY_ADD
的实现 :对应的C代码会POP
两次栈来获取操作数,调用C语言的加法函数,然后SET_TOP()
(相当于PUSH
)将结果放回栈顶。RETURN_VALUE
的实现 :它的C代码除了POP
返回值外,还会做一些额外工作,比如将当前frame
的状态设置为FRAME_RETURNED
(已返回),然后通过goto exiting
跳出主执行循环,完成函数的返回流程。
4 总结
Python程序的运行是一个层次分明、逻辑清晰的过程:
- 创建Frame :每次函数调用或程序启动时,都会创建一个新的执行环境
frame
。 - 顺序执行字节码 :在
frame
的环境下,Python虚拟机会像CPU执行汇编指令一样,一条一条地执行code object
中的字节码。 - 与Stack交互:绝大多数字节码指令都是围绕一个内部数据栈(Stack)进行操作,包括加载数据到栈、从栈中取数据计算、将结果存回栈等。
- C语言实现 :每一条字节码指令都在CPython的底层(主要是
ceval.c
)有对应的C语言代码实现。
这个机制与汇编语言在硬件上运行的模式非常相似,不同之处在于,Python的字节码是运行在一个用C语言编写的软件虚拟机之上,而不是直接运行在物理硬件上。理解了这个流程,就能对Python的运行原理有一个更深刻的认识。