在 3.2 节中我们看到,Python 源码会被编译器转换成字节码,再由虚拟机逐条执行。这一节我们就深入剖析字节码本身是什么、如何组织、以及虚拟机是怎样运行的。
字节码指令
字节码是一系列低阶操作指令,每条指令由一个操作码和一个可选操作数组成。它们与 CPU 指令相似,但运行在 Python 虚拟机之上,因此天然跨平台。
你可以通过 dis 模块查看任何函数的字节码:
import dis
def add(a, b):
return a + b
dis.dis(add)
输出类似:
2 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_ADD
6 RETURN_VALUE
LOAD_FAST将局部变量a、b推入值栈。BINARY_ADD弹出栈顶两个值相加,再将结果压栈。RETURN_VALUE弹出栈顶值,将其作为函数返回值。
这些指令就是虚拟机执行的最小单元。它们涵盖了变量读写、算术运算、控制流跳转(JUMP_FORWARD、POP_JUMP_IF_FALSE)、函数调用(CALL_FUNCTION)等所有操作。Python 版本升级时,字节码指令集也会变化,但使用 dis 总能正确反编译,这也是编写底层工具(如调试器、性能分析器)的基础。
Code 对象
Code 对象是字节码的容器,它不仅包含了字节码指令序列,还保存了执行这段代码所需的全部元信息:
- 代码的常量表
co_consts(包含整数、字符串、None等字面量); - 变量名表
co_varnames; - 栈空间大小
co_stacksize; - 行号映射
co_lnotab(用于异常追踪显示行号); - 嵌套的 Code 对象(如函数内部定义的函数)。
每个函数、模块、类定义、单条 lambda 表达式编译后都会生成一个独立的 Code 对象。你可以通过 code 属性访问它:
def square(x):
return x * x
print(square.__code__.co_consts) # (None,)
print(square.__code__.co_varnames) # ('x',)
Code 对象是只读的,确保字节码在运行时不被意外修改。当解释器执行到一个 def 语句时,它会创建一个 Code 对象并将其包装成一个函数对象,函数对象再通过 func.code 引用该 Code 对象。
栈帧执行模型
Python 虚拟机采用栈式架构。每当一个函数被调用时,解释器会为其创建一个栈帧(frame),栈帧中包含:
- 当前 Code 对象的引用;
- 局部变量和临时值的值栈;
- 指向调用者栈帧的指针(形成调用链);
- 指令指针(记录执行到哪条字节码)。
所有活跃栈帧组成一个调用栈,当函数返回时,其栈帧被销毁,值返回给调用者栈帧。
整个过程就像这样:
- 虚拟机从当前帧中取出下一条指令;
- 执行指令(操作数值栈、局部变量或全局变量);
- 遇到函数调用指令时,创建新的栈帧并切换到它;
- 返回时恢复上一帧继续执行。
这种执行模型带来了清晰的好处:
- 异常回溯 可以精确地打印出每一层栈帧的代码位置和局部变量。
- 生成器与协程 能在
yield时保留栈帧,稍后恢复执行。 - 调试器 也正是通过操控栈帧来实现执行暂停、变量检查等功能。
理解字节码和栈帧,并不是日常编程的必修课,但当你需要深度性能调优、调试诡异问题、或开发调试工具时,它就成了一把不可或缺的解剖刀。