首页
/ CPython Frame 对象 C API 深度解析:PyFrameObject、FrameLocalsProxy 写穿代理与内部帧接口

CPython Frame 对象 C API 深度解析:PyFrameObject、FrameLocalsProxy 写穿代理与内部帧接口

2026-09-04 20:17:45作者:袁立春Spencer

本篇基于 CPython 官方文档 frame.rst 展开,系统讲解 C 扩展开发者如何获取、检查和操作 Python 帧对象(frame object):涵盖 PyFrame_NewPyFrame_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_codef_linenof_locals 等字段,必须改用本文介绍的 PyFrame_Get* 系列访问器函数。这一改动对应仓库中的头文件组织:

  • Include/frameobject.h:公共入口,先包含 Include/pyframe.h(Limited C API 部分),再在 Py_LIMITED_API 未定义时包含 Include/cpython/frameobject.h
  • Include/cpython/pyframe.h:声明 PyFrame_TypePyFrameLocalsProxy_Type 两个类型对象,以及 PyFrame_GetBackPyFrame_GetLocalsPyFrame_GetGlobalsPyFrame_GetBuiltinsPyFrame_GetGeneratorPyFrame_GetLastiPyFrame_GetVarPyFrame_GetVarString 等全部访问器原型。
  • Include/cpython/frameobject.h:声明 PyFrame_New 和三个遗留同步函数(PyFrame_LocalsToFastPyFrame_FastToLocalsWithErrorPyFrame_FastToLocals),并定义了代理对象结构体:
typedef struct {
    PyObject_HEAD
    PyFrameObject* frame;
} PyFrameLocalsProxyObject;

头文件注释(Include/cpython/frameobject.h)还特别说明:_PyFrame_IsEntryFrame 是解释器实现细节层面的 API,被视为不稳定,仅为调试器、profiler 和状态检查工具提供便利,未来小版本可能改变——从源码结构看,这类带 _Py 前缀的函数均不属于稳定 ABI。

2. 如何拿到一个帧对象

文档指出,可以用 PyEval_GetFramePyThreadState_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:

  1. 先通过 _PyDict_LoadBuiltinsFromGlobals(globals) 从 globals 字典加载 __builtins__ 得到 builtins 命名空间;
  2. PyFrameConstructor(包含 globals、builtins、co_name、code 对象等)经 _PyFunction_FromConstructor 临时构造一个函数对象;
  3. 调用 _PyFrame_New_NoTrack 分配帧对象(大小由 co_nlocalsplus + co_stacksize 决定,见 Objects/frameobject.c),并把内部帧标记为 FRAME_OWNED_BY_FRAME_OBJECT,表示这个内部帧的生命周期由 PyFrameObject 本身接管;
  4. 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_lastiNone 返回 -1 3.11
PyFrame_GetVar(frame, name) 强引用或 NULL 按名称取局部变量;不存在抛 NameErrorname 必须是 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_builtinsNULL 时,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.cframelocalsproxy_getkeyindexObjects/frameobject.cframelocalsproxy_setitem 可以推断出代理的完整数据模型,由两个区域构成:

  1. 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.cObjects/frameobject.c)。
  2. f_extra_locals 字典:代理上写入"帧中不存在的名字"会懒创建该字典并存放于此;读取时 fast locals 未命中再查这里(Objects/frameobject.c)。

映射方法面(Objects/frameobject.c)包括:__getitem____contains__keysvaluesitemsgetpopsetdefaultupdatecopy__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_GetLineNumberPyFrame_GetLasti 等公共函数在内部正是调用这些 PyUnstable_ 函数(如 Objects/frameobject.c 中的 PyUnstable_InterpreterFrame_GetLine),只是额外加上了帧对象完整性断言与临界区保护。如果你实现自定义帧求值器(PyFrameEvaluationFunction,见 Doc/c-api/sys.rst 中的相关说明),处理的对象就是 _PyInterpreterFrame,此时应直接使用本节的 Unstable 接口。

7. 工程建议与版本边界

结合本文档与源码,给 C 扩展开发者的几条落地建议:

  1. 优先 Limited API:3.11 起 PyFrame_TypePyFrame_CheckPyFrame_GetBack/GetCode 等均在 Limited C API 中(Include/pyframe.h 即 Limited 部分),Py_LIMITED_API 下只需包含 Python.hpyframe.h;仅 PyFrame_New 与遗留同步函数需要非受限构建(经 Include/frameobject.hcpython/frameobject.h 路径可见)。
  2. 引用计数纪律PyFrame_NewPyFrame_GetBack/GetCode/GetGlobals/GetBuiltins/GetGenerator/GetLocals/GetVar 全部返回强引用,用完必须 Py_DECREFPyFrame_GetLastiPyFrame_GetLineNumber、各 *_Check 不涉及引用。
  3. NULL 返回的二义性PyFrame_GetBackPyFrame_GetGeneratorNULL 表示"没有外层帧/不属于生成器"而非错误(不抛异常),而 PyFrame_GetVarNULL 伴随已设置的异常——分支判断前先区分场景。
  4. 版本基线:本文 API 面以当前仓库(3.13 开发主线)为准:PyFrame_GetVar/GetVarString 需 3.12+,FrameLocalsProxy 行为需 3.13+,PyUnstable_InterpreterFrame_* 需 3.12+;三个 PyFrame_*To* 同步函数在 3.13 中为空操作。跨版本构建时建议按版本号条件编译。
  5. 不要依赖结构体成员:3.11 已移除 PyFrameObject 公共成员,任何通过 offsetof 或字段访问的内部实现(包括第三方调试器)都应迁移到本文的访问器 API;对 PEP 523 工具而言,_PyFrame_IsEntryFrame_Py 前缀接口同样可能在小版本间变化。

本文所有 API 声明可回溯至 Include/cpython/pyframe.hInclude/cpython/frameobject.h,行为实现可回溯至 Objects/frameobject.c,C API 测试见 Modules/_testcapi/frame.c

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384