CPython Frame 对象 C API 深度解析:PyFrameObject、FrameLocalsProxy 写穿代理与内部帧接口
本篇基于 CPython 官方文档 frame.rst 展开,系统讲解 C 扩展开发者如何获取、检查和操作 Python 帧对象(frame object):涵盖 PyFrame_New、PyFrame_GetVar 等公共 API 的语义与引用计数约定、3.13 引入的 PEP 667 FrameLocalsProxy 写穿代理的底层实现,以及仅供 PEP 523 自定义求值器使用的内部帧接口,帮助你在调试器、profiler、trace 工具等场景中安全地读取和修改帧状态。
1. 帧对象与 PyFrameObject:一个不透明的 C 结构体
帧对象(frame object)描述一次函数调用(或模块、类体、exec/eval 等代码块)执行时的运行时状态:正在执行的代码对象、指令指针、局部变量、全局/内建命名空间、trace 函数、外层帧指针等。Python 层的 sys._getframe() 返回的 types.FrameType 实例就是它。
C 层面的入口是 PyFrameObject 结构体类型:
// 来自 Doc/c-api/frame.rst
// The C structure of the objects used to describe frame objects.
// There are no public members in this structure.
一个关键的 API 演进事实:从 Python 3.11 起,PyFrameObject 的成员被从公共 C API 中移除,该结构体不再暴露任何公共成员(见 3.11 What's New 中的 "Hide PyFrameObject members" 条目,锚点 pyframeobject-3.11-hiding)。也就是说,C 扩展无法再直接访问 f_code、f_lineno、f_locals 等字段,必须改用本文介绍的 PyFrame_Get* 系列访问器函数。这一改动对应仓库中的头文件组织:
- Include/frameobject.h:公共入口,先包含 Include/pyframe.h(Limited C API 部分),再在
Py_LIMITED_API未定义时包含 Include/cpython/frameobject.h。 - Include/cpython/pyframe.h:声明
PyFrame_Type、PyFrameLocalsProxy_Type两个类型对象,以及PyFrame_GetBack、PyFrame_GetLocals、PyFrame_GetGlobals、PyFrame_GetBuiltins、PyFrame_GetGenerator、PyFrame_GetLasti、PyFrame_GetVar、PyFrame_GetVarString等全部访问器原型。 - Include/cpython/frameobject.h:声明
PyFrame_New和三个遗留同步函数(PyFrame_LocalsToFast、PyFrame_FastToLocalsWithError、PyFrame_FastToLocals),并定义了代理对象结构体:
typedef struct {
PyObject_HEAD
PyFrameObject* frame;
} PyFrameLocalsProxyObject;
头文件注释(Include/cpython/frameobject.h)还特别说明:_PyFrame_IsEntryFrame 是解释器实现细节层面的 API,被视为不稳定,仅为调试器、profiler 和状态检查工具提供便利,未来小版本可能改变——从源码结构看,这类带 _Py 前缀的函数均不属于稳定 ABI。
2. 如何拿到一个帧对象
文档指出,可以用 PyEval_GetFrame 和 PyThreadState_GetFrame 两个函数获取当前线程的帧对象,两者都是强引用语义(调用者负责 Py_DECREF)。此外文档将帧对象的更深层用法引导到 CPython 反射(Reflection)章节,对应仓库中的 Doc/c-api/reflection.rst。
帧对象的类型即 PyFrame_Type:
PyAPI_DATA(PyTypeObject) PyFrame_Type; // Include/cpython/pyframe.h
它等价于 Python 层的 types.FrameType。3.11 之前,这个符号只在包含 <frameobject.h> 后可见;3.11 起它移入 Limited C API(Python.h 即可使用)。类型检查宏同样在 Include/cpython/pyframe.h 中定义:
#define PyFrame_Check(op) Py_IS_TYPE((op), &PyFrame_Type)
#define PyFrameLocalsProxy_Check(op) Py_IS_TYPE((op), &PyFrameLocalsProxy_Type)
PyFrame_Check(PyObject *obj) 在 obj 是帧对象时返回非零,3.11 起同样进入 Limited C API。
3. 创建帧对象:PyFrame_New
PyFrameObject *PyFrame_New(PyThreadState *tstate, PyCodeObject *code,
PyObject *globals, PyObject *locals);
成功时返回新帧对象的强引用,失败时返回 NULL 并设置异常。原型声明位于 Include/cpython/frameobject.h。
其实现(Objects/frameobject.c)展示了内部机制,理解它有助于正确使用该 API:
- 先通过
_PyDict_LoadBuiltinsFromGlobals(globals)从 globals 字典加载__builtins__得到 builtins 命名空间; - 用
PyFrameConstructor(包含 globals、builtins、co_name、code 对象等)经_PyFunction_FromConstructor临时构造一个函数对象; - 调用
_PyFrame_New_NoTrack分配帧对象(大小由co_nlocalsplus + co_stacksize决定,见 Objects/frameobject.c),并把内部帧标记为FRAME_OWNED_BY_FRAME_OBJECT,表示这个内部帧的生命周期由PyFrameObject本身接管; - 将
instr_ptr置于首个可跟踪指令之后,使帧呈现"已完成第一条 RESUME"的完整状态,最后将对象登记进 GC。
从源码结构看,PyFrame_New 创建的帧是一个自持(self-owned)帧:不挂在任何调用栈或生成器上,因此 frame.clear() 对其合法(若是 FRAME_OWNED_BY_THREAD 的执行中帧会抛 RuntimeError,见 Objects/frameobject.c)。
4. 访问器 API 全览
下表汇总文档中列出的全部公共访问器(含版本标注与引用语义),这是 C 扩展检查帧状态的标准工具箱:
| 函数 | 返回 | 语义 | 引入版本 |
|---|---|---|---|
PyFrame_GetBack(frame) |
强引用或 NULL |
获取下一层外层帧;没有外层帧时返回 NULL,不抛异常 |
3.9 |
PyFrame_GetBuiltins(frame) |
强引用,非 NULL |
获取 f_builtins |
3.11 |
PyFrame_GetCode(frame) |
强引用,非 NULL |
获取帧正在执行的代码对象 | 3.9 |
PyFrame_GetGenerator(frame) |
强引用或 NULL |
获取拥有该帧的 generator/coroutine/async generator;不属于生成器时返回 NULL 且不抛异常 |
3.11 |
PyFrame_GetGlobals(frame) |
强引用,非 NULL |
获取 f_globals |
3.11 |
PyFrame_GetLasti(frame) |
int |
获取 f_lasti;若 f_lasti 为 None 返回 -1 |
3.11 |
PyFrame_GetVar(frame, name) |
强引用或 NULL |
按名称取局部变量;不存在抛 NameError;name 必须是 str |
3.12 |
PyFrame_GetVarString(frame, name) |
同上 | 同上,但名称为 UTF-8 编码的 C 字符串 | 3.12 |
PyFrame_GetLocals(frame) |
强引用 | 获取 f_locals;优化作用域返回写穿代理 |
3.11(3.13 起返回 PyFrameLocalsProxy_Type 实例) |
PyFrame_GetLineNumber(frame) |
int |
当前执行行号 | - |
下面结合源码逐一说明关键实现细节。
4.1 PyFrame_GetBack:如何找到"外层帧"
PyFrameObject* PyFrame_GetBack(PyFrameObject *frame)
实现见 Objects/frameobject.c:优先使用缓存的 frame->f_back;若为 NULL,则沿内部帧链 frame->f_frame->previous 回退,经 _PyFrame_GetFirstComplete 找到第一个"完整"帧后,用 _PyFrame_GetFrameObject 按需物化一个 PyFrameObject。因此该调用可能动态创建外层帧对象——这是 3.11 引入 _PyInterpreterFrame 分离设计后的典型模式:内部帧链与 Python 层帧对象解耦,帧对象惰性生成。
4.2 PyFrame_GetCode / GetGlobals / GetBuiltins
三者都是对内部帧对应字段的只读快照,返回强引用。以 PyFrame_GetCode 为例(Objects/frameobject.c):
PyCodeObject *
PyFrame_GetCode(PyFrameObject *frame)
{
assert(frame != NULL);
PyObject *code;
Py_BEGIN_CRITICAL_SECTION(frame);
assert(!_PyFrame_IsIncomplete(frame->f_frame));
code = Py_NewRef(_PyFrame_GetCode(frame->f_frame));
Py_END_CRITICAL_SECTION();
return (PyCodeObject *)code;
}
注意它使用了临界区(Py_BEGIN_CRITICAL_SECTION)保护,确保在多线程/子解释器环境下读取 code 对象期间帧不会被并发销毁——3.11 之后所有访问器普遍采用这一写法。PyFrame_GetGlobals/PyFrame_GetBuiltins 则是直接委托给对应属性的 getter 实现(Objects/frameobject.c):当内部帧的 f_globals/f_builtins 为 NULL 时,getter 会返回 None 的强引用,所以"结果不能为 NULL"指的是 C 层指针,而非值不为 None。
4.3 PyFrame_GetGenerator
PyObject* PyFrame_GetGenerator(PyFrameObject *frame)
获取拥有该帧的生成器、协程或异步生成器;若帧不属于任何生成器则返回 NULL,不抛异常。实现(Objects/frameobject.c)检查内部帧的 owner == FRAME_OWNED_BY_GENERATOR,命中时通过 _PyGen_GetGeneratorFromFrame 反向找回生成器对象。
4.4 PyFrame_GetLasti:指令指针的字节偏移
PyFrame_GetLasti 返回 f_lasti,即上次执行指令的偏移;若为 None 则返回 -1。源码(Objects/frameobject.c)中有一个值得注意的换算:内部帧保存的 lasti 以"指令单元"(_Py_CODEUNIT)为单位,公共 API 将其乘以 sizeof(_Py_CODEUNIT) 换算为字节偏移,并在临界区内读取:
int lasti = _PyInterpreterFrame_LASTI(frame->f_frame);
ret = lasti < 0 ? -1 : lasti * (int)sizeof(_Py_CODEUNIT);
如果你同时使用 dis 模块分析字节码,需要保持这一单位约定一致。
4.5 PyFrame_GetVar / PyFrame_GetVarString(3.12 新增)
PyObject* PyFrame_GetVar(PyFrameObject *frame, PyObject *name)
PyObject* PyFrame_GetVarString(PyFrameObject *frame, const char *name)
按名称读取帧中名为 name 的局部变量。语义要点:
- 成功时返回变量值的强引用;
- 变量不存在时抛
NameError并返回NULL; - name 的类型必须是
str,否则抛TypeError。
实现(Objects/frameobject.c)先校验 PyUnicode_Check(name),再调用 frame_init_get_vars 处理尚未执行的 COPY_FREE_VARS(闭包帧的 free 变量初始化),然后线性扫描 co->co_localsplusnames 匹配名称;匹配后由 frame_get_var 依据 co_localspluskinds 区分普通局部变量、cell 变量(解引用 PyCellObject 取值)和 free 变量。PyFrame_GetVarString 只是把 C 字符串包成 str 后委托给前者(Objects/frameobject.c)。
这两个函数的 C API 测试位于 Modules/_testcapi/frame.c,可结合 test.testcapi 查看行为边界。
4.6 PyFrame_GetLineNumber
返回帧当前执行的行号。实现(Objects/frameobject.c)会先用缓存的 f->f_lineno,为 -1 时调用 PyUnstable_InterpreterFrame_GetLine 现算;取不到行号时把缓存置 0 并返回 -1。
5. FrameLocalsProxy:PEP 667 的写穿局部变量代理(3.13)
这是本文档最具版本敏感度的部分。自 Python 3.13 起,优化作用域(函数/生成器帧)中 frame.f_locals 不再返回"局部变量字典的快照",而是返回一个 frame-locals proxy 实例:它暴露底层局部变量的写穿(write-through)视图,保证通过 f_locals 看到和修改的永远是帧内"活的"局部变量。设计动机与完整提案见 PEP 667。
5.1 类型与检查函数
PyAPI_DATA(PyTypeObject) PyFrameLocalsProxy_Type; // 帧 locals 代理的类型
int PyFrameLocalsProxy_Check(PyObject *obj); // 是帧 locals 代理则非零
PyFrameLocalsProxy_Type 与检查宏见 Include/cpython/pyframe.h;类型对象定义(tp_name = "FrameLocalsProxy",实现了 Py_TPFLAGS_MAPPING 映射协议)见 Objects/frameobject.c。Python 层可这样验证:
import sys, types
def f():
frame = sys._getframe()
loc = frame.f_locals
print(type(loc).__name__) # FrameLocalsProxy
x = 1
loc["y"] = 42 # 写穿到帧
print(x, frame.f_locals["y"])
5.2 何时返回代理、何时直接返回字典
文档说明:优化作用域返回写穿代理;类、模块、exec/eval 等场景则直接返回表示帧 locals 的 mapping。这与源码完全一致(Objects/frameobject.c):
if (!(co->co_flags & CO_OPTIMIZED) && !_PyFrame_HasHiddenLocals(self->f_frame)) {
...
return Py_NewRef(self->f_frame->f_locals); // 非优化作用域:直接返回字典
}
return _PyFrameLocalsProxy_New(self); // 优化作用域:返回代理
_PyFrame_HasHiddenLocals 是 PEP 709(内联推导式)引入的例外:当帧带有隔离的"hidden fast locals"时,即便是非优化代码也要走代理,以便对这些隐藏变量做过滤。
5.3 代理的内部机制:fast locals + extra locals
从 Objects/frameobject.c 的 framelocalsproxy_getkeyindex 和 Objects/frameobject.c 的 framelocalsproxy_setitem 可以推断出代理的完整数据模型,由两个区域构成:
- fast locals 数组(内部帧的
localsplus槽位):代理按名称在co->co_localsplusnames中定位索引(优先做指针比较以利用字符串驻留,未命中再走哈希+PyObject_RichCompareBool慢路径),读值直接读槽位;对CO_FAST_FREE/CO_FAST_CELL类型会解引用 cell(Objects/frameobject.c)。向 fast local 赋None会抛ValueError("cannot remove local variables from FrameLocalsProxy")——你不能通过代理"删除"一个已声明的局部变量,只能改值。写 fast local 时,若原值非 immortal,旧对象会被登记进f_overwritten_fast_locals元组,保证帧退出时引用被正确释放(Objects/frameobject.c、Objects/frameobject.c)。 f_extra_locals字典:代理上写入"帧中不存在的名字"会懒创建该字典并存放于此;读取时 fast locals 未命中再查这里(Objects/frameobject.c)。
映射方法面(Objects/frameobject.c)包括:__getitem__、__contains__、keys、values、items、get、pop、setdefault、update、copy、__reversed__,并额外实现了 |/|= 合并语义(Objects/frameobject.c);pop 对 fast local 同样拒绝删除。迭代顺序为"fast locals 按 co_localsplusnames 顺序 + extra locals"。
PyFrame_GetLocals(frame)(3.11 引入,3.13 起返回上述代理)就是获取这一视图的 C 层入口,实现上直接复用了 f_locals 属性 getter(Objects/frameobject.c)。
5.4 遗留同步 API:现在什么都不做
代理机制落地后,三个历史同步函数被软弃用——3.13 中保留符号但变为空操作,仅为向后兼容而存在:
void PyFrame_LocalsToFast(PyFrameObject *f, int clear);
void PyFrame_FastToLocals(PyFrameObject *f);
int PyFrame_FastToLocalsWithError(PyFrameObject *f);
3.13 之前,PyFrame_LocalsToFast 负责把 f_locals 的修改同步进"fast locals"数组(clear 为真时处理被 unset 的变量),PyFrame_FastToLocals 做反向同步。现在的实现(Objects/frameobject.c)明确注释原因:
// Nothing to do here, as f_locals is now a write-through proxy in
// optimized frames. Soft-deprecated, since there's no maintenance hassle.
实战含义:如果你的 C 扩展在 sys.settrace/sys.setprofile 回调里曾手动调用这三个函数做"局部变量双向同步",升级到 3.13 后这些调用是安全的空操作,且可以安全移除——因为代理已保证 f_locals 永远实时。
6. 内部帧接口:面向 PEP 523 自定义求值器
文档的最后一节标注"除非使用 PEP 523,否则你不需要这些"。_PyInterpreterFrame 是解释器的内部帧表示(3.11 引入),是 3.11 性能改造的核心:求值栈上流动的是紧凑的内部帧,Python 层 PyFrameObject 只是按需生成的包装。
c:struct:: _PyInterpreterFrame // 内部帧表示,3.11 新增
配套提供三个带 PyUnstable_ 前缀的只读访问器(原型与语义注释见 Include/cpython/pyframe.h):
| 函数 | 返回 | 语义 |
|---|---|---|
PyUnstable_InterpreterFrame_GetCode(frame) |
代码对象的强引用 | 不抛异常 |
PyUnstable_InterpreterFrame_GetLasti(frame) |
int |
最后执行指令的字节偏移;不抛异常 |
PyUnstable_InterpreterFrame_GetLine(frame) |
int |
当前执行行号,无行号时返回 -1;不抛异常 |
头文件注释明确其用途:"for use by debuggers and other tools implementing custom frame evaluators with PEP 523"。从源码结构看,它们与本文第 4 节公共 API 的关系是:PyFrame_GetLineNumber、PyFrame_GetLasti 等公共函数在内部正是调用这些 PyUnstable_ 函数(如 Objects/frameobject.c 中的 PyUnstable_InterpreterFrame_GetLine),只是额外加上了帧对象完整性断言与临界区保护。如果你实现自定义帧求值器(PyFrameEvaluationFunction,见 Doc/c-api/sys.rst 中的相关说明),处理的对象就是 _PyInterpreterFrame,此时应直接使用本节的 Unstable 接口。
7. 工程建议与版本边界
结合本文档与源码,给 C 扩展开发者的几条落地建议:
- 优先 Limited API:3.11 起
PyFrame_Type、PyFrame_Check、PyFrame_GetBack/GetCode等均在 Limited C API 中(Include/pyframe.h 即 Limited 部分),Py_LIMITED_API下只需包含Python.h或pyframe.h;仅PyFrame_New与遗留同步函数需要非受限构建(经 Include/frameobject.h 的cpython/frameobject.h路径可见)。 - 引用计数纪律:
PyFrame_New、PyFrame_GetBack/GetCode/GetGlobals/GetBuiltins/GetGenerator/GetLocals/GetVar全部返回强引用,用完必须Py_DECREF;PyFrame_GetLasti、PyFrame_GetLineNumber、各*_Check不涉及引用。 NULL返回的二义性:PyFrame_GetBack与PyFrame_GetGenerator的NULL表示"没有外层帧/不属于生成器"而非错误(不抛异常),而PyFrame_GetVar的NULL伴随已设置的异常——分支判断前先区分场景。- 版本基线:本文 API 面以当前仓库(3.13 开发主线)为准:
PyFrame_GetVar/GetVarString需 3.12+,FrameLocalsProxy行为需 3.13+,PyUnstable_InterpreterFrame_*需 3.12+;三个PyFrame_*To*同步函数在 3.13 中为空操作。跨版本构建时建议按版本号条件编译。 - 不要依赖结构体成员:3.11 已移除
PyFrameObject公共成员,任何通过offsetof或字段访问的内部实现(包括第三方调试器)都应迁移到本文的访问器 API;对 PEP 523 工具而言,_PyFrame_IsEntryFrame等_Py前缀接口同样可能在小版本间变化。
本文所有 API 声明可回溯至 Include/cpython/pyframe.h 与 Include/cpython/frameobject.h,行为实现可回溯至 Objects/frameobject.c,C API 测试见 Modules/_testcapi/frame.c。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00