首页
/ CPython C API 反射机制详解:PyEval_GetFrame、PyEval_GetFrameLocals 与 PEP 667 迁移指南

CPython C API 反射机制详解:PyEval_GetFrame、PyEval_GetFrameLocals 与 PEP 667 迁移指南

2026-09-06 11:58:48作者:房伟宁

本文围绕 CPython 官方文档 Reflection(反射) 展开,系统讲解 PyEval_GetBuiltinsPyEval_GetLocalsPyEval_GetGlobalsPyEval_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.cbreakpoint()/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);
}

两个要点:

  1. 回退逻辑明确:有帧时取 frame->f_builtins,无帧时回退到 tstate->interp->builtins,与文档"or the interpreter of the thread state"的描述一致。
  2. 返回借用引用:旧函数直接返回内部指针,不增加引用计数。调用者不得 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_GetLocalslocalsframe.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_cacheInclude/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_GetBuiltinsPyEval_GetGlobalsPyEval_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 calling locals in 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();
}

实现逻辑清晰:

  1. 无帧时同样设置 SystemError 并返回 NULL
  2. 对帧调用 _PyFrame_GetLocals;若得到的是 FrameLocalsProxy(优化作用域的写透代理),就新建一个 dictPyDict_Update 出当前快照返回——这正是 locals() 在 3.13 中的快照语义;
  3. 若得到的是普通 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 扩展开发者的实际影响:

  1. 迁移映射(官方文档给出的替换关系,与 What's New 一致):
    • PyEval_GetBuiltinsPyEval_GetFrameBuiltins
    • PyEval_GetGlobalsPyEval_GetFrameGlobals
    • PyEval_GetLocalsPyEval_GetFrameLocals
  2. 引用计数处理必须改写:旧代码把返回值当借用引用使用(不 DECREF);迁移到新 API 后必须为每次成功调用补上 Py_DECREF,否则泄漏。
  3. 依赖"共享缓存字典"语义的代码会行为变化:若有工具依赖"对 PyEval_GetLocals 的多次调用得到同一个可修改的 dict",应改为:
    • 需要快照语义:改用 PyEval_GetFrameLocals
    • 需要"读穿/写透帧变量"语义:改用 PyFrame_GetLocals(PyEval_GetFrame()),文档在 PyEval_GetFrameLocals 条目中明确建议了这一路径。
  4. 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.cPy_XNewRef(_PyEval_GetBuiltins(tstate))),因此不会返回 NULL
  • 不要混用旧借用引用语义与新强引用语义:例如在 Py_LIMITED_API 环境下调用旧 PyEval_GetLocals 后误加 Py_DECREF,会提前销毁仍被帧内部引用的字典。

八、常见陷阱小结

  1. 引用类型混淆:旧 GetBuiltins/GetGlobals/GetLocals 返回借用引用;新 GetFrameBuiltins/GetGlobals/GetLocals 返回强引用。迁移时漏加 Py_DECREF 造成泄漏,是 PEP 667 相关升级中最常见的问题。
  2. PyEval_GetLocals 的缓存字典陷阱:在优化作用域它返回帧上缓存的同一个 dict 并原地更新(Python/ceval.cf_locals_cache 逻辑);若你的工具期望"每次拿到一份独立快照",请改用 PyEval_GetFrameLocals
  3. PyEval_GetFuncName 对非可调用对象返回类型名:文档明确"else the name of func's type",示例中 PyEval_GetFuncDesc 返回 " object" 即用于此场景(Python/ceval.c)。
  4. 线程相关性:所有 PyEval_Get* 函数基于"当前附加的线程状态"(_PyThreadState_GET()),跨线程操作应改用显式接收 PyThreadState * 的内部/线程状态 API(如 PyThreadState_GetFrame,见 Python/pystate.c)。

九、关键源码与文档索引

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