CPython C API 反射机制详解:PyEval_GetFrame、PyEval_GetFrameLocals 与 PEP 667 迁移指南
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 扩展需要反射式 APIPython 的帧对象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.hPyAPI_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_GetBuiltins3.13 起弃用文档定义返回当前执行帧的 builtins 字典若当前线程没有正在执行的帧则返回该线程所属解释器的 builtins。3.13 起标记为 deprecated建议改用PyEval_GetFrameBuiltins。源码印证Python/ceval.cPyObject * _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_GetLocals3.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详见 Whats New in Python 3.13 中Defined mutation semantics for locals 一节。源码印证Python/ceval.cPyObject * 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_GetGlobals3.13 起弃用文档定义返回当前执行帧的全局变量字典没有帧时返回NULL。3.13 起建议改用PyEval_GetFrameGlobals。源码印证Python/ceval.cstatic 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.cPyFrameObject * 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。这一点在 Whats 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.cPyObject * _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.cPyObject* 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_XNewRefPyEval_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_GetBuiltins3.13 弃用借用引用回退到解释器 builtins近似__builtins__返回值不得 DECREFPyEval_GetLocals3.13 弃用借用引用NULLSystemError旧式locals缓存语义优化作用域返回同一缓存 dict重复调用原地更新PyEval_GetGlobals3.13 弃用借用引用NULL近似globals()返回f_globals借用引用PyEval_GetFrame长期存在强引用NULL—可配合PyFrame_GetLocals使用PyEval_GetFrameBuiltins3.13 新增强引用回退到解释器 builtins不为 NULL近似__builtins__必须 DECREFPyEval_GetFrameLocals3.13 新增强引用NULLSystemErrorlocals()优化作用域返回独立快照PyEval_GetFrameGlobals3.13 新增强引用NULLglobals()必须 DECREF五、PEP 667 迁移指南从旧 API 到新 APIWhats New in Python 3.13 对该变化有一段完整说明3.13 起优化作用域函数、生成器、协程、推导式、生成器表达式中locals()显式返回当前已赋值的局部变量含被闭包捕获的局部引用的非局部变量的独立快照而frame.f_locals在这些作用域中改为返回写透代理write-through proxy以便调试器能可靠地更新局部变量。这对 C 扩展开发者的实际影响迁移映射官方文档给出的替换关系与 Whats 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.cconst 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_DECREFbuiltins 的兜底PyEval_GetFrameBuiltins即使无帧也会回退到解释器 builtinsPython/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 offuncs 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.cPyEval_GetFrame、PyEval_GetBuiltins、PyEval_GetLocals、_PyEval_GetFrameLocals、PyEval_GetFrameGlobals、PyEval_GetFrameBuiltins、PyEval_GetFuncName、PyEval_GetFuncDescf_locals_cache兼容字段注释Include/internal/pycore_frame.hPyFrame_GetLocals声明Include/cpython/pyframe.hPEP 667 语义变更说明Doc/whatsnew/3.13.rst3.13 新增强引用 API 记录Doc/whatsnew/3.13.rst【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考