人人都会AI编程

3.3 字节码机制:字节码指令、Code 对象、栈帧执行模型

更新时间:2026-07-12

在 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 将局部变量 ab 推入值栈
  • BINARY_ADD 弹出栈顶两个值相加,再将结果压栈。
  • RETURN_VALUE 弹出栈顶值,将其作为函数返回值。

这些指令就是虚拟机执行的最小单元。它们涵盖了变量读写、算术运算、控制流跳转(JUMP_FORWARDPOP_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 对象的引用;
  • 局部变量和临时值的值栈
  • 指向调用者栈帧的指针(形成调用链);
  • 指令指针(记录执行到哪条字节码)。

所有活跃栈帧组成一个调用栈,当函数返回时,其栈帧被销毁,值返回给调用者栈帧。

整个过程就像这样:

  1. 虚拟机从当前帧中取出下一条指令;
  2. 执行指令(操作数值栈、局部变量或全局变量);
  3. 遇到函数调用指令时,创建新的栈帧并切换到它;
  4. 返回时恢复上一帧继续执行。

这种执行模型带来了清晰的好处:

  • 异常回溯 可以精确地打印出每一层栈帧的代码位置和局部变量。
  • 生成器与协程 能在 yield 时保留栈帧,稍后恢复执行。
  • 调试器 也正是通过操控栈帧来实现执行暂停、变量检查等功能。

理解字节码和栈帧,并不是日常编程的必修课,但当你需要深度性能调优、调试诡异问题、或开发调试工具时,它就成了一把不可或缺的解剖刀。