CPython C API 反射机制详解:PyEval_GetFrame、PyEval_GetFrameLocals 与 PEP 667 迁移指南
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
本文围绕 CPython 官方文档 Reflection(反射) 展开,系统讲解PyEval_GetBuiltins、PyEval_GetLocals、PyEval_GetGlobals、PyEval_GetFrame等用于从 C 扩展"向内窥视"当前执行帧(frame)的反射式 C API,以及 Python 3.13 中 PEP 667 引入的三个新函数PyEval_GetFrameBuiltins/PyEval_GetFrameLocals/PyEval_GetFrameGlobals的语义差异与迁移方法。读完本文,你可以在 C 扩展、调试器或代码追踪工具中正确读取当前帧的局部变量、全局变量与内置函数表,并正确管理借用引用(borrowed reference)与强引用(strong reference)的引用计数,避免悬垂指针与内存泄漏。
一、为什么 C 扩展需要"反射"式 API
Python 的帧对象(frame)在执行期间持有三块命名空间:局部变量(locals)、全局变量(globals)和内置命名空间(builtins)。C 扩展运行在解释器进程内部,经常需要回答这样的问题:
- 当前正在执行的 Python 代码是哪个函数?
- 它当前作用域里的局部变量、全局变量分别是什么?
- 当前帧使用了哪套
__builtins__?
这类"从 C 代码反向获取 Python 运行时上下文"的能力,在 CPython 中由ceval.c中的一组PyEval_Get*函数提供,官方文档将其归入 Reflection 一章。这些函数被 CPython 内部大量使用,例如 Python/bltinmodule.c 中breakpoint()/help()相关路径、Python/import.c 的导入逻辑,以及 Python/legacy_tracing.c 的帧追踪回调,都依赖_PyEval_GetFrame()判断"当前线程是否正处于 Python 帧执行中"。
所有公共声明位于 Include/ceval.h:
PyAPI_FUNC(PyObject *) PyEval_GetBuiltins(void); PyAPI_FUNC(PyObject *) PyEval_GetGlobals(void); PyAPI_FUNC(PyObject *) PyEval_GetLocals(void); PyAPI_FUNC(PyFrameObject *) PyEval_GetFrame(void); PyAPI_FUNC(PyObject *) PyEval_GetFrameBuiltins(void); PyAPI_FUNC(PyObject *) PyEval_GetFrameGlobals(void); PyAPI_FUNC(PyObject *) PyEval_GetFrameLocals(void);二、旧版三个函数:语义、引用计数与弃用状态
2.1 PyEval_GetBuiltins(3.13 起弃用)
文档定义:返回当前执行帧的 builtins 字典;若当前线程没有正在执行的帧,则返回该线程所属解释器的 builtins。3.13 起标记为 deprecated,建议改用PyEval_GetFrameBuiltins。
源码印证(Python/ceval.c):
PyObject * _PyEval_GetBuiltins(PyThreadState *tstate) { _PyInterpreterFrame *frame = _PyThreadState_GetFrame(tstate); if (frame != NULL) { return frame->f_builtins; } return tstate->interp->builtins; } PyObject * PyEval_GetBuiltins(void) { PyThreadState *tstate = _PyThreadState_GET(); return _PyEval_GetBuiltins(tstate); }两个要点:
- 回退逻辑明确:有帧时取
frame->f_builtins,无帧时回退到tstate->interp->builtins,与文档"or the interpreter of the thread state"的描述一致。 - 返回借用引用:旧函数直接返回内部指针,不增加引用计数。调用者不得
Py_DECREF返回值,若需长期持有必须先Py_INCREF。
CPython 内部就有一个典型用法:_PyEval_GetBuiltin()(Python/ceval.c)通过PyEval_GetBuiltins()按名字查找内建函数。
2.2 PyEval_GetLocals(3.13 起弃用):最复杂的引用语义
文档定义:返回一个映射(mapping),提供对当前执行帧局部变量的访问;没有帧时返回NULL。其返回值的语义在 3.13 中经历 PEP 667 的重大变化,是这组 API 中最容易踩坑的函数。
关键事实(来自 Doc/c-api/reflection.rst 与源码):
- 旧函数返回的是借用引用(borrowed reference)。
- 在优化作用域(optimized scope,即函数、生成器、协程、推导式等)中,返回值是缓存在该帧对象上的同一个字典,只要帧对象存活它就存活;对同一帧的后续调用会更新这个缓存字典的内容以反映局部变量的最新状态,而不是返回新的快照。
- 3.13 起,
PyFrame_GetLocals、locals与frame.f_locals不再使用这个共享缓存字典(PEP 667),详见 What's New in Python 3.13 中"Defined mutation semantics for locals" 一节。
源码印证(Python/ceval.c):
PyObject * PyEval_GetLocals(void) { // We need to return a borrowed reference here, so some tricks are needed PyThreadState *tstate = _PyThreadState_GET(); _PyInterpreterFrame *current_frame = _PyThreadState_GetFrame(tstate); if (current_frame == NULL) { _PyErr_SetString(tstate, PyExc_SystemError, "frame does not exist"); return NULL; } // Be aware that this returns a new reference PyObject *locals = _PyFrame_GetLocals(current_frame); ... if (PyFrameLocalsProxy_Check(locals)) { PyFrameObject *f = _PyFrame_GetFrameObject(current_frame); ... PyObject *ret = f->f_locals_cache; if (ret == NULL) { ret = PyDict_New(); ... f->f_locals_cache = ret; } if (PyDict_Update(ret, locals) < 0) { ... } Py_DECREF(locals); return ret; } ... }可以观察到:
- 无帧时设置
SystemError("frame does not exist")并返回NULL,因此 C 调用方必须检查NULL并处理异常。 - 注释明确写着 "We need to return a borrowed reference here, so some tricks are needed"——为了维持"借用引用 + 同一帧多次调用返回同一缓存字典"的旧语义,实现上需要专门维护
f->f_locals_cache。Include/internal/pycore_frame.h 中对该字段的注释也说明它"纯粹是为了向后兼容 PyEval_GetLocals"而存在:因为旧 API 要求借用引用,实际返回的字典需要一个强引用存放在帧对象里维持存活。 - 在 3.13 中
_PyFrame_GetLocals于优化作用域返回的是FrameLocalsProxy(写透代理),旧函数会把它物化进f_locals_cache字典并原地更新,这正是文档所说"后续调用更新缓存字典内容"的底层机制。
2.3 PyEval_GetGlobals(3.13 起弃用)
文档定义:返回当前执行帧的全局变量字典;没有帧时返回NULL。3.13 起建议改用PyEval_GetFrameGlobals。
源码印证(Python/ceval.c):
static PyObject * _PyEval_GetGlobals(PyThreadState *tstate) { _PyInterpreterFrame *current_frame = _PyThreadState_GetFrame(tstate); if (current_frame == NULL) { return NULL; } return current_frame->f_globals; }与 builtins 不同,PyEval_GetGlobals在没有帧时直接返回NULL,不会回退到任何全局对象——这与文档"or NULL if no frame is currently executing"严格一致。返回的是帧中f_globals的借用引用。
三、PyEval_GetFrame:获取当前帧对象
文档定义:返回"attached thread state(附加线程状态)"的帧对象;当前没有帧在执行时返回NULL。文档同时提示参见PyThreadState_GetFrame(两者等价,后者接受显式的PyThreadState *参数,适合跨线程操作场景)。
源码印证(Python/ceval.c):
PyFrameObject * PyEval_GetFrame(void) { _PyInterpreterFrame *frame = _PyEval_GetFrame(); if (frame == NULL) { return NULL; } PyFrameObject *f = _PyFrame_GetFrameObject(frame); if (f == NULL) { PyErr_Clear(); } return f; }返回值是强引用,调用方在使用完毕后必须Py_DECREF。典型用途是作为其他 API 的输入:文档在PyEval_GetFrameLocals一节中明确指出,如果想在不生成独立快照的情况下访问当前帧的f_locals,应调用PyFrame_GetLocals(PyEval_GetFrame())(PyFrame_GetLocals声明见 Include/cpython/pyframe.h)。
四、3.13 新 API:返回强引用的三个 GetFrame* 函数
PEP 667 将"修改locals()返回值的语义"标准化后,CPython 3.13 新增了三个返回强引用的函数,分别取代旧的PyEval_GetBuiltins、PyEval_GetGlobals、PyEval_GetLocals。这一点在 What's New in Python 3.13 的 C API 变化 中有明确记录:"Add new functions that return a strong reference instead of a borrowed reference for frame locals, globals, and builtins, as part of PEP 667"。
4.1 PyEval_GetFrameLocals:等价于 Python 层的 locals()
文档定义:返回当前执行帧局部变量的字典;无帧时返回NULL。"Equivalent to callinglocalsin Python code"——即在优化作用域中返回独立的快照字典(snapshot),而不是旧 API 那种原地更新的共享缓存。
源码印证(Python/ceval.c):
PyObject * _PyEval_GetFrameLocals(void) { PyThreadState *tstate = _PyThreadState_GET(); _PyInterpreterFrame *current_frame = _PyThreadState_GetFrame(tstate); if (current_frame == NULL) { _PyErr_SetString(tstate, PyExc_SystemError, "frame does not exist"); return NULL; } PyObject *locals = _PyFrame_GetLocals(current_frame); if (locals == NULL) { return NULL; } if (PyFrameLocalsProxy_Check(locals)) { PyObject* ret = PyDict_New(); ... if (PyDict_Update(ret, locals) < 0) { ... } Py_DECREF(locals); return ret; } assert(PyMapping_Check(locals)); return locals; } PyObject* PyEval_GetFrameLocals(void) { return _PyEval_GetFrameLocals(); }实现逻辑清晰:
- 无帧时同样设置
SystemError并返回NULL; - 对帧调用
_PyFrame_GetLocals;若得到的是FrameLocalsProxy(优化作用域的写透代理),就新建一个dict并PyDict_Update出当前快照返回——这正是locals()在 3.13 中的快照语义; - 若得到的是普通 mapping(如模块级作用域直接就是 globals 字典),则直接返回它(此时为强引用)。
因此PyEval_GetFrameLocals每次调用都返回独立的强引用,调用方必须Py_DECREF。
4.2 PyEval_GetFrameGlobals 与 PyEval_GetFrameBuiltins
源码印证(Python/ceval.c):
PyObject* PyEval_GetFrameGlobals(void) { PyThreadState *tstate = _PyThreadState_GET(); _PyInterpreterFrame *current_frame = _PyThreadState_GetFrame(tstate); if (current_frame == NULL) { return NULL; } return Py_XNewRef(current_frame->f_globals); } PyObject* PyEval_GetFrameBuiltins(void) { PyThreadState *tstate = _PyThreadState_GET(); return Py_XNewRef(_PyEval_GetBuiltins(tstate)); }两者都是对旧实现的"包一层Py_XNewRef":
PyEval_GetFrameGlobals等价于 Python 层globals():无帧返回NULL,有帧返回f_globals的强引用;PyEval_GetFrameBuiltins复用_PyEval_GetBuiltins的"有帧取frame->f_builtins、无帧回退tstate->interp->builtins"逻辑,并用Py_XNewRef把借用引用升级为强引用——所以它永不返回NULL(除非引用转换失败),与旧PyEval_GetBuiltins的回退行为保持一致。
4.3 新旧 API 速查表
| 函数 | 引入/弃用 | 返回引用类型 | 无帧时行为 | 等价 Python | 备注 |
|---|---|---|---|---|---|
PyEval_GetBuiltins | 3.13 弃用 | 借用引用 | 回退到解释器 builtins | 近似__builtins__ | 返回值不得 DECREF |
PyEval_GetLocals | 3.13 弃用 | 借用引用 | NULL+SystemError | 旧式locals缓存语义 | 优化作用域返回同一缓存 dict,重复调用原地更新 |
PyEval_GetGlobals | 3.13 弃用 | 借用引用 | NULL | 近似globals() | 返回f_globals借用引用 |
PyEval_GetFrame | 长期存在 | 强引用 | NULL | — | 可配合PyFrame_GetLocals使用 |
PyEval_GetFrameBuiltins | 3.13 新增 | 强引用 | 回退到解释器 builtins(不为 NULL) | 近似__builtins__ | 必须 DECREF |
PyEval_GetFrameLocals | 3.13 新增 | 强引用 | NULL+SystemError | locals() | 优化作用域返回独立快照 |
PyEval_GetFrameGlobals | 3.13 新增 | 强引用 | NULL | globals() | 必须 DECREF |
五、PEP 667 迁移指南:从旧 API 到新 API
What's New in Python 3.13 对该变化有一段完整说明:3.13 起,优化作用域(函数、生成器、协程、推导式、生成器表达式)中locals()显式返回当前已赋值的局部变量(含被闭包捕获的局部引用的非局部变量)的独立快照;而frame.f_locals在这些作用域中改为返回"写透代理"(write-through proxy),以便调试器能可靠地更新局部变量。
这对 C 扩展开发者的实际影响:
- 迁移映射(官方文档给出的替换关系,与 What's New 一致):
PyEval_GetBuiltins→PyEval_GetFrameBuiltinsPyEval_GetGlobals→PyEval_GetFrameGlobalsPyEval_GetLocals→PyEval_GetFrameLocals
- 引用计数处理必须改写:旧代码把返回值当借用引用使用(不 DECREF);迁移到新 API 后必须为每次成功调用补上
Py_DECREF,否则泄漏。 - 依赖"共享缓存字典"语义的代码会行为变化:若有工具依赖"对
PyEval_GetLocals的多次调用得到同一个可修改的 dict",应改为:- 需要快照语义:改用
PyEval_GetFrameLocals; - 需要"读穿/写透帧变量"语义:改用
PyFrame_GetLocals(PyEval_GetFrame()),文档在PyEval_GetFrameLocals条目中明确建议了这一路径。
- 需要快照语义:改用
exec/eval类隐式局部命名空间行为:在优化作用域中,不再显式传递命名空间的exec/eval现在总是针对一个独立快照运行,其改动不会反映到后续的locals()调用中;若需读回改动,必须显式传入命名空间引用(来源:Doc/whatsnew/3.13.rst)。
六、PyEval_GetFuncName 与 PyEval_GetFuncDesc:生成"函数描述"
文档还记录了两个用于生成人类可读描述的辅助函数,常见于__repr__风格的展示(如"<function foo at 0x...>")。
PyEval_GetFuncName(PyObject *func):若参数是函数、类或实例对象,返回它的名字;否则返回其类型的名字。
PyEval_GetFuncDesc(PyObject *func):根据类型返回一段描述字符串,文档列出的返回值包括()``、" constructor"、`" instance"、" object"``;与PyEval_GetFuncName` 的返回值拼接后,即可得到对func的完整描述。
源码印证(Python/ceval.c):
const char * PyEval_GetFuncName(PyObject *func) { if (PyMethod_Check(func)) return PyEval_GetFuncName(PyMethod_GET_FUNCTION(func)); else if (PyFunction_Check(func)) return PyUnicode_AsUTF8(((PyFunctionObject*)func)->func_name); else if (PyCFunction_Check(func)) return ((PyCFunctionObject*)func)->m_ml->ml_name; else return Py_TYPE(func)->tp_name; } const char * PyEval_GetFuncDesc(PyObject *func) { if (PyMethod_Check(func)) return "()"; else if (PyFunction_Check(func)) return "()"; else if (PyCFunction_Check(func)) return "()"; else return " object"; }从源码结构看,当前实现中PyEval_GetFuncDesc的分支只有两种实际取值:方法/Python 函数/C 函数返回"()",其余对象返回" object";文档中列出的" constructor"、" instance"等取值描述的是该 API 的设计契约(历史上用于构造"<class 'A' constructor>"一类字符串)。另外注意两者都返回const char *静态字符串或对象内部 UTF-8 指针,不能 free,且仅在对象存活期间有效(如PyUnicode_AsUTF8的结果依赖对象生命周期)。
七、实战:在 C 扩展中安全地捕获当前帧快照
下面给出一个综合示例,展示 3.13+ 新 API 的正确用法(引用计数完整、异常路径可追踪):
#define PY_SSIZE_T_CLEAN #include <Python.h> /* 在某个 Python 帧执行期间被调用(例如通过 sys.settrace 触发的回调) */ static PyObject * dump_frame_snapshot(PyObject *self, PyObject *args) { PyObject *locals = PyEval_GetFrameLocals(); /* 强引用,等价 locals() */ PyObject *globals = PyEval_GetFrameGlobals(); /* 强引用,等价 globals() */ PyObject *builtins = PyEval_GetFrameBuiltins();/* 强引用,永不 NULL */ if (locals == NULL || globals == NULL) { Py_XDECREF(locals); Py_XDECREF(globals); return NULL; /* 错误已在 _PyEval_GetFrameLocals 中设置(无帧时为 SystemError) */ } PyObject *result = Py_BuildValue("(OOO)", locals, globals, builtins); Py_DECREF(locals); Py_DECREF(globals); Py_DECREF(builtins); return result; }使用约束与注意事项(均来自上文文档与源码证据):
- 必须在有 Python 帧执行的线程上调用:
PyEval_GetFrameLocals/PyEval_GetFrameGlobals无帧时返回NULL并设置异常(或仅返回NULL),调用前可用PyEval_GetFrame()探测(其返回强引用,用后Py_DECREF); - builtins 的兜底:
PyEval_GetFrameBuiltins即使无帧也会回退到解释器 builtins(Python/ceval.c 中Py_XNewRef(_PyEval_GetBuiltins(tstate))),因此不会返回NULL; - 不要混用旧借用引用语义与新强引用语义:例如在
Py_LIMITED_API环境下调用旧PyEval_GetLocals后误加Py_DECREF,会提前销毁仍被帧内部引用的字典。
八、常见陷阱小结
- 引用类型混淆:旧
GetBuiltins/GetGlobals/GetLocals返回借用引用;新GetFrameBuiltins/GetGlobals/GetLocals返回强引用。迁移时漏加Py_DECREF造成泄漏,是 PEP 667 相关升级中最常见的问题。 PyEval_GetLocals的缓存字典陷阱:在优化作用域它返回帧上缓存的同一个 dict 并原地更新(Python/ceval.c 的f_locals_cache逻辑);若你的工具期望"每次拿到一份独立快照",请改用PyEval_GetFrameLocals。PyEval_GetFuncName对非可调用对象返回类型名:文档明确"else the name offunc's type",示例中PyEval_GetFuncDesc返回" object"即用于此场景(Python/ceval.c)。- 线程相关性:所有
PyEval_Get*函数基于"当前附加的线程状态"(_PyThreadState_GET()),跨线程操作应改用显式接收PyThreadState *的内部/线程状态 API(如PyThreadState_GetFrame,见 Python/pystate.c)。
九、关键源码与文档索引
- API 文档:Doc/c-api/reflection.rst
- 公共声明:Include/ceval.h
- 核心实现:Python/ceval.c(
PyEval_GetFrame、PyEval_GetBuiltins、PyEval_GetLocals、_PyEval_GetFrameLocals、PyEval_GetFrameGlobals、PyEval_GetFrameBuiltins、PyEval_GetFuncName、PyEval_GetFuncDesc) f_locals_cache兼容字段注释:Include/internal/pycore_frame.hPyFrame_GetLocals声明:Include/cpython/pyframe.h- PEP 667 语义变更说明:Doc/whatsnew/3.13.rst
- 3.13 新增强引用 API 记录:Doc/whatsnew/3.13.rst
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考