Chapter 28: Execution State and Frame
执行 Python 函数时,解释器需要保存当前代码执行到哪里、局部变量放在哪里、表达式中间值还留在哪、异常回溯怎样连接上一层调用。frame 就是这组执行状态的承载对象。读完本章后,读者应能定位一个函数调用对应的 frame,判断 f_locals、f_back、traceback、generator frame 各自保存什么状态,并能复盘一个 frame 从创建、运行、挂起到释放的生命周期。
本章以 CPython 为主讲实现,并以 Python 3.14 文档作为版本边界。官方 C API 把 PyFrameObject 定义为描述 frame object 的 C 结构,同时说明 Python 3.11 以后该结构已经移除公开成员,扩展代码应通过访问器读取状态,见 Python/C API: Frame objects。Python 层的可观察入口主要来自 inspect 的 interpreter stack API、traceback 对象和 generator 对象属性。
贯穿本章的材料是一段普通调用和一段可挂起执行。普通调用用来观察当前 frame、调用者 frame 和局部变量;generator 用来观察同一段代码如何在 yield 处暂停并保留执行状态。
import inspect
def leaf(value):
local_value = value + 1
frame = inspect.currentframe()
try:
return {
"function": frame.f_code.co_name,
"caller": frame.f_back.f_code.co_name,
"locals": dict(frame.f_locals),
}
finally:
del frame
def caller(seed):
return leaf(seed * 2)
def suspended(seed):
local_value = seed + 1
yield local_value
return local_value + 1
这段代码里,caller(10) 会进入 caller 的 frame,再进入 leaf 的 frame。leaf 中的 inspect.currentframe() 返回当前调用栈顶层的 Python frame。suspended(10) 会先产生 generator object;执行推进到 yield 时,局部变量 local_value 和下一条执行位置会留在 generator 关联的 frame 中。普通函数返回后,frame 通常退出执行路径;generator 在关闭前会把 frame 当成对象状态的一部分保留下来。
28.1 PyFrameObject and frame stack
PyFrameObject 是 Python 层 frame object 在 CPython C API 中的类型名称。frame object 保存一段 code object 的一次执行现场,包括当前执行的 code object、globals、locals、builtins、上一层 frame、当前指令位置和调试追踪相关字段。Python 层看到的 frame.f_code、frame.f_globals、frame.f_locals、frame.f_back、frame.f_lasti、frame.f_lineno,就是这些执行状态的观察窗口。
frame stack 是 Python 调用栈的可观察形态。一次 Python 函数调用会把新的执行状态压到当前线程的执行链路上;函数返回、异常传播完成或 frame 被挂起时,这个状态会离开普通调用路径。leaf 执行期间,栈顶是 leaf 的 frame,frame.f_back 指向 caller 的 frame,caller 的 f_back 再指向更外层调用者。这个链条提供了“当前函数由谁调用”的信息,也为 traceback 和 inspect.stack() 这类工具提供数据来源。
def leaf(value):
local_value = value + 1
frame = inspect.currentframe()
try:
return frame.f_code.co_name, frame.f_back.f_code.co_name
finally:
del frame
这段代码读取两个名字:frame.f_code.co_name 来自当前 frame 的 code object,结果是 leaf;frame.f_back.f_code.co_name 来自上一层 frame,结果是 caller。这里的关键关系是:function object 描述“可调用代码”,code object 描述“编译后的静态指令与元数据”,frame 描述“某一次执行”。同一个 function 可以被调用多次,每次调用都有自己的 frame 状态;同一个 code object 可以被多个 frame 共享。
CPython 3.11 以后,源码阅读要区分公开 PyFrameObject 与解释器内部 frame 表示。公开 C API 文档已经把 PyFrameObject 视为 opaque struct,扩展代码通过 PyFrame_GetCode()、PyFrame_GetBack()、PyFrame_GetLocals() 这类函数读取状态。解释器执行循环可以使用内部 _PyInterpreterFrame 保存更贴近 eval loop 的字段;当调试、traceback、inspect 或 C API 需要 Python 对象时,才暴露出 frame object 语义。工程判断上,语言层保证的是可观察的 frame 属性和 introspection 行为,内部布局属于 CPython 版本敏感实现。
frame stack 与操作系统线程栈属于两个层级。Python frame 描述 Python 代码执行状态;C 调用栈描述解释器自身的 C 函数调用。调试 Python 程序时,inspect 和 traceback 看到的是 Python frame 链;调试 CPython 源码时,C debugger 还会看到 eval loop、vectorcall、对象分配等 C 层函数。混合阅读时,应先确认自己正在分析 Python 调用关系,还是 CPython 解释器自身的 C 调用关系。
28.2 Value stack, block stack, and fast locals
frame 内部状态可以按三组职责理解:value stack 保存表达式中间值,fast locals 保存编译期确定的局部变量槽位,block stack 或等价的异常处理状态保存控制流清理边界。这个拆分能把“源码一行表达式”还原成解释器执行时的对象移动路径。
value stack 是 stack VM 的临时对象通道。表达式 seed * 2 在进入 leaf 之前,会把 seed 和常量 2 取到栈上,执行乘法 opcode,得到的结果再作为参数进入调用路径。leaf 内部的 local_value = value + 1 也会先把 value 和 1 放入 value stack,完成加法后再把结果写入局部变量槽位。value stack 的存在解释了一个现象:表达式中间对象在名字绑定前也可能被 frame 暂时持有,异常或调试工具观察执行现场时,局部变量表只覆盖已经落入局部槽位或 locals 视图的那部分状态。
fast locals 是函数优化作用域中的局部变量存储方式。编译器在 code object 中记录参数名和局部变量名,例如 co_varnames、co_nlocals,解释器用索引访问对应槽位。local_value 在源码中是名字,在执行时会被编译期符号分析映射到 fast locals 的某个位置。读取局部变量时,解释器走索引访问;展示 frame.f_locals 时,解释器再把这些槽位以 mapping 形态暴露给 Python 层。
Python 3.13 以后,frame.f_locals 在优化作用域中返回 frame-locals proxy。这个 proxy 是对底层 fast locals 的写穿透视图,相关行为来自 PEP 667 并已进入官方 frame C API 文档。它解决的是调试器、trace function 和运行中 frame 修改局部变量时的同步问题。locals() 在优化作用域中走另一条语义:它返回当前局部变量的快照。判断 frame.f_locals 与 locals() 的差异时,应先确认 Python 版本,再确认当前作用域是函数优化作用域、模块作用域、类作用域,还是 exec / eval 提供的显式 namespace。
block stack 是阅读旧版本 CPython frame 时常见的名字,用来记录循环、异常处理、finally 等控制流块的退出边界。Python 3.11 引入 zero-cost exception table 后,很多异常处理入口和栈展开信息从显式 bytecode setup 迁移到 exception table 与解释器状态中。源码阅读时仍然可以保留这个判断模型:控制流块需要回答“异常或跳转发生时,应把 value stack 收缩到哪个深度,应进入哪个 handler,应恢复哪类异常状态”。在 3.10 及更早版本,这个问题常落在 frame 的 block stack;在 3.11 以后,这个问题更多落在 exception table、handler 入口和 unwind 过程。
把三组状态放回贯穿代码,caller(seed) 的参数 seed 进入 fast locals,seed * 2 的中间值经过 value stack,调用 leaf 后新 frame 获得自己的 fast locals。leaf 中 try...finally 的清理边界进入异常和退出路径的控制流状态。finally: del frame 的作用是切断当前局部变量对 frame object 的引用,减少由 frame、locals、对象引用形成的循环引用风险。
28.3 Traceback and frame introspection
traceback 是异常传播时留下的 frame 链快照。异常对象的 __traceback__ 指向 traceback 链;每个 traceback 节点保存一个 tb_frame、一个 tb_lasti、一个 tb_lineno 和指向下一层的 tb_next。tb_frame 让错误报告能够回到具体函数、文件、行号和局部变量;tb_lasti 让工具定位最后尝试执行的 bytecode 位置;tb_next 把异常从外层调用连接到真正抛出位置。
下面的代码把普通调用改成失败路径,用来观察 traceback 如何引用 frame。
def leaf(value):
local_value = value + 1
raise RuntimeError(local_value)
def caller(seed):
return leaf(seed * 2)
try:
caller(10)
except RuntimeError as error:
traceback_head = error.__traceback__
traceback_head 的第一层通常对应 except 所在的外层执行位置,沿着 tb_next 往里走,会经过 caller,最后到达 leaf 抛出异常的位置。每一层 traceback 都能找到对应的 frame。这个结构解释了为什么日志系统和调试器可以在异常发生后打印调用栈,也解释了为什么保留异常对象可能延长局部对象生命周期:异常持有 traceback,traceback 持有 frame,frame 持有 locals,locals 又可能持有大对象、文件句柄或业务对象。
inspect 提供的是主动 introspection。inspect.currentframe() 读取当前 frame,inspect.getouterframes(frame) 沿 f_back 读取外层 frame,inspect.stack() 直接生成当前调用栈的 FrameInfo 列表。官方文档明确提示,保留 frame 引用会制造引用循环,延长 frame 可访问对象的生命周期;因此示例中使用 finally: del frame。如果工具需要保存错误现场,应提取文件名、函数名、行号、少量局部变量摘要,再释放 frame 引用;需要保留完整 frame 时,应在使用完成后调用 frame.clear() 或清理外部引用。
introspection 的工程边界来自两个事实。第一,inspect.currentframe() 依赖解释器支持 Python stack frame,官方文档把它标注为 CPython implementation detail;在其它 Python 实现中可能返回 None。第二,frame 展示的是执行现场,读取 f_locals、f_back、f_code 可能改变对象生命周期,也可能触发工具自身的性能成本。生产环境采样、日志增强和调试器实现应把 frame 读取限制在明确边界内,并尽早把 frame 转成普通数据结构。
28.4 Frame suspension and generator frame
generator 把 frame 从“调用期间临时存在”变成“对象内部保存的暂停状态”。普通函数调用会立即进入执行,返回时退出 frame。generator function 调用先返回 generator object,函数体要到第一次 next() 或 send(None) 时才开始执行。执行到 yield 后,解释器把当前位置、fast locals、value stack 中需要保留的状态留在 generator 关联的 frame 中,下一次恢复时从 yield 后继续执行。
def suspended(seed):
local_value = seed + 1
yield local_value
return local_value + 1
gen = suspended(10)
first = next(gen)
gen = suspended(10) 产生 generator object,suspended 的函数体还没有推进到 local_value = seed + 1。first = next(gen) 让 frame 进入执行,local_value 被写入 fast locals,随后 yield local_value 把值交给调用方,并让 frame 停在 yield 表达式附近。此时 generator object 拥有该 frame;inspect.getgeneratorstate(gen) 会把状态归类为 created、running、suspended 或 closed 之一,官方文档把这些状态用于判断 generator 当前是否等待启动、正在执行、暂停在 yield,或已经结束。
暂停状态的核心价值是保留局部对象和下一步位置。local_value 在 yield 后仍然可用于 return local_value + 1,说明它没有像普通函数返回那样被释放。这个行为支撑了惰性迭代、协程调度和 async generator。它也带来生命周期成本:generator 长时间处于 suspended 状态时,frame 中的局部变量会继续持有对象。大型缓冲区、数据库连接、锁或临时上下文如果被局部变量引用,就会跟随 generator 生命周期延长。
coroutine 和 async generator 共享同一类判断模型。coroutine 在 await 处暂停,async generator 在异步 yield 处暂停;对象属性名和状态 API 有差异,但问题仍然是:当前对象是否拥有一个可恢复的 frame,frame 停在哪个位置,locals 中还持有哪些对象,恢复时会从哪个 await 或 yield 之后继续。后续章节会专门展开 async runtime,本节只建立 frame 视角:挂起执行意味着 frame 生命周期脱离普通调用返回路径,转而受 generator、coroutine 或 async generator object 控制。
28.5 Frame lifecycle
frame lifecycle 可以按五个阶段复盘:创建、执行、观察、挂起或退出、释放。这个顺序适用于普通函数、异常路径和 generator,只是各阶段停留时间和引用所有权不同。
创建阶段把 code object、globals、builtins、参数绑定结果和局部变量布局组合成一次执行现场。普通函数在调用时创建并进入执行;generator function 调用会创建 generator object,并把执行现场留到首次推进时进入运行。CPython 内部是否立即创建完整 Python 层 PyFrameObject 属于版本敏感实现;稳定判断应落到可观察行为:函数执行有 frame 语义,introspection、traceback、profiling 和 generator 暂停可以让这个语义变成 Python 对象。
执行阶段由 eval loop 推进 instruction pointer,并在 value stack 与 fast locals 之间移动对象。调用其它 Python 函数时,当前 frame 暂停在调用指令附近,新 frame 成为栈顶;被调用方返回后,返回值进入调用方的 value stack,调用方继续推进。异常传播时,解释器查找当前 frame 是否有 handler;有 handler 时进入 handler,没有 handler 时退出当前 frame 并把异常交给上一层。
观察阶段由调试器、profile hook、trace function、inspect、traceback 或 C API 触发。观察会把 frame 变成普通 Python 可引用对象。只要外部还持有 frame,frame 中可到达的局部变量就会继续存活。这个阶段的工程结论很明确:调试工具应记录必要信息,并释放 frame;业务代码中直接保存 frame 应有明确释放点。
挂起阶段发生在 generator、coroutine 和 async generator 中。frame 离开当前调用栈顶,但没有完成;它被对应对象保存,等待下一次 next()、send()、throw()、事件循环调度或关闭动作。挂起 frame 的检查顺序是:先看对象状态,再看是否仍有关联 frame,再看 locals 中保留的对象,最后看恢复入口和关闭路径。
释放阶段要求没有外部强引用继续持有 frame。普通返回路径中,如果 frame 没有被 traceback、inspect 结果、闭包对象、局部对象循环或调试器持有,它就会随执行结束进入释放或复用路径。异常路径中,异常对象和 traceback 会延长 frame 生命周期。generator 路径中,generator closed 后关联 frame 才能释放。阅读内存泄漏、资源延迟释放或调试工具开销时,应把 frame 当作一组强引用的汇合点来排查。
把本章方法压缩成一个检查顺序:先确定执行对象是普通函数、异常 traceback,还是 generator/coroutine;再定位 frame 的 f_code、f_back、f_globals、f_locals;接着区分 fast locals、value stack 临时值和控制流清理状态;然后判断是否有 traceback、inspect 结果或 suspended object 延长生命周期;最后根据 Python 版本确认 f_locals、exception table 和 C API 访问器的行为边界。这个顺序能把“函数为什么还没释放”“traceback 为什么能看到局部变量”“generator 为什么记得上次位置”统一解释为 frame 状态和引用关系问题。
最小自检任务
阅读下面代码,判断 snapshot、captured_error 和 gen 分别可能让哪些执行状态继续存活,并说明 del frame 解决的是哪一类引用关系。
import inspect
def capture(value):
local_value = [value]
frame = inspect.currentframe()
try:
snapshot = dict(frame.f_locals)
return snapshot
finally:
del frame
def fail(value):
local_value = [value]
raise RuntimeError("boom")
def paused(value):
local_value = [value]
yield len(local_value)
snapshot = capture(1)
try:
fail(2)
except RuntimeError as error:
captured_error = error
gen = paused(3)
next(gen)
答案要点
snapshot 是普通 dict,它保存了从 frame.f_locals 复制出来的键值。这里的 local_value 是列表对象,snapshot 会继续持有这个列表;它不再需要 frame 本身参与读取。finally: del frame 切断的是 capture 局部变量 frame 对当前 frame object 的引用,降低 frame 与局部变量之间形成引用循环的风险。
captured_error 持有异常对象,异常对象持有 __traceback__,traceback 节点持有 tb_frame。因此 fail 的 frame 以及其中的 local_value 列表可能因为异常对象被保存而继续存活。清理这类状态时,应清掉异常引用或 traceback 引用,或者在需要时使用 traceback 提取普通数据后释放原始对象。
gen 是 generator object。next(gen) 推进到 yield 后,paused 的 frame 处于 suspended 状态,local_value 仍在该 frame 的 locals 中。只要 gen 还未关闭并且仍被外部引用,这个 frame 就可能继续持有 local_value。关闭 generator 或让它执行完成后,关联 frame 才能进入释放路径。
本章知识点总结
- frame 定位:frame 保存一段 code object 的一次执行现场,包含名字空间、调用者、指令位置和调试状态。
- 调用栈:
f_back把当前 frame 连接到上一层 Python frame,形成 Python 层可观察调用链。 - 对象分层:function object 描述可调用对象,code object 描述静态编译结果,frame 描述一次动态执行。
- 公开边界:Python 3.11 以后
PyFrameObject公共 C API 移除结构成员,扩展代码应使用访问器读取 frame 状态。 - value stack:value stack 保存表达式求值过程中的中间对象,局部变量表无法覆盖所有临时执行状态。
- fast locals:函数优化作用域把参数和局部变量放入索引槽位,
f_locals负责把槽位暴露成 mapping 视图。 - locals 版本:Python 3.13 以后优化作用域中的
frame.f_locals是写穿透 proxy,locals()返回当前快照。 - 控制流状态:block stack 是旧版本 CPython 的显式控制流块记录;现代 CPython 还要结合 exception table 和 unwind 过程判断异常路径。
- traceback 链:traceback 节点通过
tb_frame、tb_lasti、tb_lineno和tb_next保存异常发生时的调用路径。 - 观察成本:
inspect和 traceback 会让 frame 成为可引用对象,保留 frame 会延长 locals 可达对象的生命周期。 - 挂起执行:generator 在
yield处保存 frame,locals 和下一步位置随 generator object 一起存活。 - 生命周期:frame 的释放取决于返回路径、异常 traceback、introspection 引用、generator 状态和外部强引用。
- 排查顺序:分析执行状态时先分类普通调用、异常或挂起对象,再查 frame 属性、局部状态、控制流状态和引用持有者。