You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

嵌入式Python调用Py_CompileString等API时对象无法释放问题

问题根因

这不是Python的预期行为,是嵌入式场景下模块卸载流程缺失关键步骤导致的引用残留,核心问题出在PyImport_ExecCodeModule()的隐式行为:

  • 该API执行时会自动将生成的模块对象以传入的模块名为键,存入全局模块缓存sys.modules,并持有1个强引用。你自己代码里拿到的模块指针只是第二个引用,仅释放自己持有的引用时,sys.modules里的引用始终存在,模块引用计数永远剩1,不会被销毁。

你观察到的引用计数现象完全可以顺着这个逻辑解释:

  • 编译得到的code对象初始引用计数为1:该对象仅由你的代码持有,没有被Python内部全局结构引用,调用Py_DECREF()后引用计数归0直接释放,此时指针变为悬空野值是正常表现,不要在对象释放后再访问该指针。
  • 模块、函数、返回值初始引用计数为2:
    • 1个引用来自你调用API时拿到的返回值,由你的C++代码持有
    • 剩余1个引用来自Python内部的引用链:模块被sys.modules持有,模块内定义的python_main函数被模块的__dict__属性字典持有,返回值如果被模块全局作用域、闭包或内部缓存引用,也会随模块一同留存
  • 你当前的“重编译生效”本质是在旧模块的命名空间内执行新编译的代码,新代码覆盖了同名的顶层变量(比如python_main函数),但旧模块本身从未被销毁,未被覆盖的旧对象、被其他路径引用的旧对象会一直残留,既会造成内存泄漏,也可能出现旧脚本注册的回调、全局状态未被清理的异常。
正确的模块清理/重加载流程

要实现旧模块彻底销毁、从零初始化的效果,重编译前必须按以下步骤操作:

  1. 从全局模块缓存中删除对应模块的条目,释放全局持有的强引用
    // PyImport_GetModuleDict返回borrowed引用,无需手动释放
    PyObject* sys_modules = PyImport_GetModuleDict();
    // 替换为你实际使用的模块名
    if (PyDict_DelItemString(sys_modules, "user_script") < 0) {
        // 模块不存在时忽略异常即可
        PyErr_Clear();
    }
    
  2. 释放你代码中持有的所有相关对象的强引用,包括但不限于:之前拿到的脚本返回值对象、python_main函数对象、模块对象、编译得到的code对象。所有Py_CompileString/PyImport_ExecCodeModule/PyObject_GetAttrString/PyObject_Call*返回的新引用,用完都要对应调用Py_DECREF()。
  3. 调用PyGC_Collect()触发一次全量垃圾回收,处理脚本对象间可能存在的循环引用——这类引用无法靠引用计数机制自动释放,必须由GC遍历回收。
额外注意事项
  • 如果需要频繁重加载脚本,不要反复使用同一个模块名:哪怕清理流程完全正确,旧模块里的对象如果被其他全局结构(比如注册的C回调、其他导入了该模块的扩展模块、Python内部类型缓存)持有引用,依然无法完全回收。可以每次重加载时给模块加自增后缀生成唯一名称(比如user_script_1/user_script_2),同时定期清理sys.modules里的过期条目,避免无限制的内存增长。
  • 不要在对象调用Py_DECREF()后再访问其内容:当引用计数归0时Python会立即释放对象内存,此时原指针是悬空野值,读取任何字段都是未定义行为。
  • 检查所有Python API返回值的引用类型:部分API返回borrowed引用(比如PyImport_GetModuleDict、PyList_GetItem)不需要手动DECREF,部分返回新引用必须手动DECREF,错漏都会导致引用计数异常。

内容的提问来源于stack exchange,提问作者Mark Coniglio

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 10:24:12