CPython 修复 OSError 子类引用泄漏:属性在 `super().__init__()` 之前设置时的内存管理修复

发布时间:2026/9/10 1:07:28
CPython 修复 OSError 子类引用泄漏:属性在 `super().__init__()` 之前设置时的内存管理修复 CPython 修复 OSError 子类引用泄漏属性在super().__init__()之前设置时的内存管理修复【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpythonoutput文章CPython 修复 OSError 子类引用泄漏属性在super().__init__()之前设置时的内存管理修复output文章CPython OSError 引用泄漏修复gh-150988子类在调用super().__init__()之前设置属性导致的内存泄漏及其修复导读本篇文章聚焦于 CPython 官方仓库中的一项内存泄漏修复当OSError子类在构造函数中、super().__init__()之前设置errno、strerror、filename等属性时CPython 存在引用泄漏reference leak问题。本文将从修复的 news 条目出发深入 Objects/exceptions.c 的异常对象内存管理实现剖析泄漏的根源、修复方式、官方回归测试以及该修复对 Python 开发者的实际意义。一、修复的背景news 条目原文与问题定位本次修复记录在 CPython 仓库的 news 文件 Misc/NEWS.d/next/Core_and_Builtins/2026-06-05-22-52-41.gh-issue-150988.fDKfMJ.rst 中原文内容为Fix a reference leak inOSErrorwhen attributes are set beforesuper().__init__().这条 news 条目属于Core_and_Builtins分类说明该修复发生在 CPython 的核心解释器层C 语言实现而非纯 Python 标准库层。它对应 GitHub issue 编号 gh-150988属于 next 版本即当前开发分支的下一版本的变更记录。问题的本质是Python 允许异常类拥有__dict__实例字典。当开发者定义一个OSError子类并在__init__中先给self设置属性、再调用super().__init__()时OSError 的 C 层初始化逻辑会重新分配内部字段而旧值没有被正确释放导致引用计数泄漏——异常对象被垃圾回收后被占用的对象如字符串对象仍停留在内存中。二、为什么是 OSError—— C 层异常的结构化字段设计要理解这个泄漏首先需要理解 CPython 异常对象在 C 层的特殊设计。与普通的 Python 对象不同BaseException及其子类在 C 层直接嵌入了多个对象指针字段见 Objects/exceptions.c 中的PyBaseExceptionObject和PyOSErrorObject。2.1 OSError 的专用字段OSError 拥有自己的 C 结构体PyOSErrorObject其中包含以下字段定义于 Objects/exceptions.c 附近的OSError_members表中属性名C 字段含义errnomyerrno操作系统错误码int 或 strstrerrorstrerror错误描述字符串filenamefilename涉及的文件名单文件操作如open()filename2filename2第二个文件名双文件操作如rename()winerrorwinerrorWindows 错误码仅 Windows 平台编译这些字段之所以被内置在结构体中而不是放入普通的实例__dict__是因为性能OSError是异常处理中最频繁的路径直接访问结构体字段比查字典快得多兼容性except OSError, (errno, strerror)这种旧式解包依赖args元组的前两个元素C 层解析保证了args的一致性见 Objects/exceptions.c 的注释跨平台一致性Windows 上winerror到errno的映射由winerror_to_errno()完成见 Objects/exceptions.c。2.2 从注释看 OSError 的设计初衷在 Objects/exceptions.c 的注释中明确写道Where a function has a single filename, such asopen()or some of theosmodule functions,PyErr_SetFromErrnoWithFilename()is called, giving a third argument which is the filename. But, so that old code using in-place unpacking doesnt break, e.g.except OSError, (errno, strerror):we hack args so that it only contains two items.这段注释揭示了两个关键点OSError的args元组被修剪为只保留前两个元素errno, strerror文件名被拆出来单独存储这是为了保持except OSError, (errno, strerror):这种老式解包语法的兼容性。三、泄漏的根源__init__的二次调用与引用覆盖3.1 正常路径OSError_new与OSError_init的分工CPython 在 Objects/exceptions.c 中实现了OSError_new__new__在 Objects/exceptions.c 中实现了OSError_init__init__。正常情况下OSError_new分配对象、解析参数、调用oserror_init一次性完成所有字段赋值随后OSError_init检测到oserror_use_init()返回 0即__init__就是 C 层的OSError_init本身直接返回 0不再重复初始化。3.2 泄漏路径子类先设属性再调super().__init__()问题出在子类自定义了__init__的场景。CPython 通过oserror_use_init()见 Objects/exceptions.c检测这种情况if (type-tp_init ! OSError_init type-tp_new OSError_new) { return 1; /* 子类定义了 __init__推迟参数解析 */ }此时OSError_new会跳过oserror_init的字段赋值仅将self-args设为空元组见 Objects/exceptions.c。然后子类的__init__执行用户代码先设置属性class LeakingOSError(OSError): def __init__(self, code, message, filename, filename2): self.strerror message # ← 先设置属性 self.filename filename self.filename2 filename2 super().__init__(code, message, filename, None, filename2) # ← 后调用父类 __init__关键点在于Python 允许通过__dict__为异常对象设置任意属性PyBaseExceptionObject中dict字段的存在就是为了这个。self.strerror message会通过PyObject_GenericSetAttr创建/写入实例字典__dict__。然后super().__init__()会触发 C 层的OSError_init或oserror_init执行Py_XSETREF(self-strerror, Py_XNewRef(strerror));Py_XSETREF宏会先Py_DECREF旧值再赋新值——这里self-strerror的旧值来自__dict__的引用路径是独立的与__dict__中的值形成双路径引用。由于OSError的属性 getter 逻辑通过OSError_members表的_Py_T_OBJECT类型定义在属性读取时优先返回结构体字段而__dict__中的同名键在OSError_init时没有被清理最终__dict__中的引用成为泄漏残留。更精确地说在修复前oserror_init通过Py_XSETREF(self-strerror, ...)覆盖结构体字段时旧字段值被正确释放但__dict__中由用户先设置的strerror/filename/filename2键值一直保留到对象销毁——而这些值在__dict__与结构体字段之间形成了对同一对象的重复引用。在OSError的子类重新调用__init__即二次初始化时旧值被覆盖但引用未全部释放造成泄漏。四、修复方式从源码结构推断由于当前仓库中该 news 条目对应的补丁已合入从 Objects/exceptions.c 的当前实现可以看到修复的痕迹OSError_clear见 Objects/exceptions.c中对myerrno、strerror、filename、filename2以及 Windows 下的winerror逐一执行Py_CLEAR再调用BaseException_clear清理dict、args、traceback、cause、context。修复的核心思路从当前代码推断是在oserror_init重新初始化字段时同步清理__dict__中对应的键或确保__dict__中的旧引用在覆盖前被释放从而避免__dict__与结构体字段之间对同一对象的双重引用残留。具体到本修复官方回归测试 Lib/test/test_exceptions.py 提供了最直接的验证def test_oserror_reinit_leak(self): # gh-150988: Check for memory leak when re-initializing OSError. # Previously, setting OSError attributes in a subclass # before calling super().__init__() leaked memory. class LeakingOSError(OSError): def __init__(self, code, message, filename, filename2): self.strerror message self.filename filename self.filename2 filename2 super().__init__(code, message, filename, None, filename2) exc LeakingOSError(1, some message, filename.py, filename2.py) exc.__init__(2, another message, filename3.py, filename4.py)注意测试的关键在于最后一行手动再次调用exc.__init__(...)对同一个对象执行二次初始化。这正是泄漏被放大的场景——第一次__init__后__dict__已含strerror/filename/filename2第二次__init__再次先写__dict__、再调super().__init__()结构体字段被Py_XSETREF覆盖但__dict__中的旧引用残留导致泄漏。4.1 测试为何能验证泄漏该测试通过test.support框架运行。在 CPython 的测试体系中内存泄漏检测通常配合gc模块和weakref使用若修复前存在泄漏二次__init__后strerror指向的字符串对象some message 等引用计数无法归零对象无法被回收修复后这些引用被正确释放。测试虽未显式断言只构造对象但配合test_exceptions.py中已有的引用计数校验机制能在--failfast与内存泄漏检测模式下暴露回归。五、修复的实际影响谁会被影响5.1 受影响的使用模式以下两类代码在修复前存在泄漏风险# 模式一子类先设属性再调 super().__init__() class MyOSError(OSError): def __init__(self, code, msg, fn): self.filename fn # 先设置 super().__init__(code, msg, fn) # 后初始化 # 模式二对同一异常对象重复调用 __init__如测试中的 reinit exc MyOSError(1, a, f1.py) exc.__init__(2, b, f2.py) # 二次初始化5.2 不受影响的使用模式# 安全模式直接调用 super().__init__()之后不再设置同名属性 class SafeOSError(OSError): def __init__(self, code, msg, fn): super().__init__(code, msg, fn) self.extra custom # 设置 OSError 无关的自定义属性安全5.3 对扩展模块作者的启示对于编写 C 扩展、使用PyErr_SetFromErrnoWithFilename等 API 的开发者本修复强调了异常对象字段管理的规范性不要在__init__中既通过__dict__写入 OSError 专有字段名又依赖super().__init__()的结构体字段赋值两者会造成引用路径的重复管理。六、深入源码OSError 完整生命周期6.1 创建OSError_new的错误处理在 Objects/exceptions.c 中OSError_new的错误路径统一走goto error依次Py_XDECREF(args)和Py_XDECREF(self)保证分配失败时无泄漏。这个模式与本次修复的目标一致任何初始化路径都不应残留未释放的引用。6.2 字段清理OSError_clear与BaseException_clear对象销毁时OSError_dealloc见 Objects/exceptions.c调用OSError_clear后者先清理 OSError 专有字段再调用BaseException_clear见 Objects/exceptions.c清理dict、args、traceback、cause、context、notes。Py_CLEAR宏同时将指针置 NULL 并Py_DECREF避免重复释放。6.3 循环引用安全OSError_traverseObjects/exceptions.c 中的OSError_traverse将 OSError 的所有字段暴露给 GC 的循环引用检测器。由于异常对象可以包含任意对象包括自身tp_traverse的完整性至关重要——这也是为什么本修复必须同时保证__dict__引用的一致清理否则 GC 遍历会出现悬垂引用或漏回收。七、如何验证与复现7.1 复现脚本在修复前的 CPython 版本上运行以下脚本可观察泄漏配合gc与weakref检测import gc import weakref class LeakingOSError(OSError): def __init__(self, code, message, filename, filename2): self.strerror message self.filename filename self.filename2 filename2 super().__init__(code, message, filename, None, filename2) refs [] for i in range(1000): exc LeakingOSError(1, msg, f1.py, f2.py) exc.__init__(2, msg2, f3.py, f4.py) refs.append(weakref.ref(exc)) del exc gc.collect() print(存活对象数:, sum(1 for r in refs if r() is not None))修复前该脚本会打印非零的存活对象数修复后应全部回收输出 0。7.2 运行官方回归测试仓库中的测试文件 Lib/test/test_exceptions.py 包含本修复的回归测试test_oserror_reinit_leak运行方式./python -m test test_exceptions -m test_oserror_reinit_leak或在完整测试中执行./python -m test test_exceptions7.3 在修复前后对比修复前exc.__init__(2, ...)二次初始化后another message/filename3.py/filename4.py 之外第一轮的值some message 等在__dict__中残留引用计数泄漏修复后__dict__与结构体字段保持一致旧引用在覆盖时被正确Py_DECREF对象可完整回收。八、更广泛的内存管理背景本次修复是 CPython 持续改进异常对象内存管理的一部分。异常对象在设计上就面临双重属性系统的挑战结构体字段PyOSErrorObject内嵌快速访问C 层管理实例字典__dict__通用属性存储PyObject_GenericSetAttr管理。两者的交互需要异常谨慎。仓库中 Objects/exceptions.c 的BaseException_getset、OSError_members等表定义了大量 getter/setter正是为了在这两套属性系统之间建立一致的读写桥接。从 Lib/test/test_exceptions.py 可以看到官方对 OSError 属性读写的全面测试cases.append((OSError(2, msgStr), errno)) cases.append((OSError(2, msgStr), strerror)) cases.append((OSError(2, msgStr), filename)) cases.append((OSError(2, msgStr), filename2))这些测试覆盖了每个专有属性的读写确保任何内存管理改动不会破坏OSError的属性语义。九、总结项目内容Issuegh-150988修复文件Objects/exceptions.c回归测试Lib/test/test_exceptions.py 的test_oserror_reinit_leaknews 条目Misc/NEWS.d/next/Core_and_Builtins/2026-06-05-22-52-41.gh-issue-150988.fDKfMJ.rst影响范围OSError 子类在super().__init__()前设置 errno/strerror/filename/filename2 属性的代码修复效果二次初始化或先设属性后初始化时不再泄漏引用对于 Python 开发者而言本修复意味着在自定义 OSError 子类时应当避免在super().__init__()之前写入 OSError 的专有属性。这不仅是为了规避历史版本的泄漏 bug更是一种内存管理的良好实践——让 C 层的结构化初始化全权负责专有字段__dict__只承载自定义附加属性。通过 Objects/exceptions.c 的源码我们可以看到 CPython 如何在性能结构化字段与灵活性__dict__之间取得平衡以及一次看似微小的 news 条目背后是怎样的内存管理严谨性。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询