注意: 虽然 JavaScript 对于本网站不是必需的,但您与内容的互动将受到限制。请开启 JavaScript 以获得完整的体验。

模块清理的改进建议

模块清理的改进建议

我正在尝试一种在执行结束后更好地清理资源的方法。在不实现真正GC(垃圾回收)的情况下,我永远无法做到百分之百正确,但我可以根据实际观察结果,实现一套可预测的规则,以解决大多数实际存在的问题。

这是我的提案。在本邮件的末尾,我列出了该提案可能存在的一些问题,并请求反馈。这可能将在Python 1.5.1中实现。

目录

修订版

根据我收到的一些评论和进一步的思考,我自上次在网上发布此主题以来,对它做了一些修改。文本中用 [方括号中的斜体批注] 表示重要的更改。

算法

当一个Python解释器被删除时,它的变量和模块会以部分指定的顺序“小心地清除”。“小心地清除”操作的定义如下;它有效地以部分指定的顺序删除模块的变量。

  • M1. 在其他任何操作之前,以下变量被设置为None(不一定按此顺序)
    • __builtin__._
    • sys.exc_{type,value,traceback}
    • sys.last_{type,value,traceback}
    • sys.path
    • sys.argv
    • sys.ps1, sys.ps2
    • sys.exitfunc
    [path, argv, ps1, ps2 和 exitfunc 是此列表中的新增项。]

  • M2. 三个标准I/O文件(sys.stdin, sys.stdout 和 sys.stderr)恢复到它们的初始值(当解释器启动时,它们分别被保存为 sys.__stdin__、sys.__stdout__ 和 sys.__stderr__)。如果任何初始值不可用,则相应的对象被设置为 None。[新增。]

  • M3. 在其他任何模块之前,小心地清除模块 __main__。[这以前是在下一步之后完成的。]

  • M4. 反复遍历所有模块,寻找引用计数为1的模块。每个引用计数为1的模块都被小心地清除。当不再找到引用计数为1的模块时,循环停止。模块 __builtin__ 和 sys 被排除在循环之外。

  • M5. 小心地清除所有剩余模块,除了 __builtin__ 和 sys。

  • M6. 小心地清除 sys。

  • M7. 小心地清除 __builtin__。

要小心地清除一个模块,需要执行以下步骤:

  • C1. 按照名称的字典哈希值确定的顺序,将所有恰好以一个下划线开头的名称设置为 None。

  • C2. 按照名称的字典哈希值确定的顺序,将除 __builtins__ 之外的所有名称设置为 None。[这以前是“所有不以两个或更多下划线开头的名称”。]

  • [已删除的步骤:按照名称的字典哈希值确定的顺序,从模块的字典中删除所有剩余的名称(通过调用 __dict__.clear() 完成)。]

  • C3. 模块本身在模块字典 (sys.modules) 中被 None 替换。

[新增。] 步骤 C1-C2 也将在模块解除分配时使用。虽然模块通常不涉及循环(除非存在相互递归的导入),但模块的字典通常涉及循环,因为模块中定义的每个函数和方法都引用其 __dict__,并且这些函数和方法通常可从该 __dict__ 访问。因此,当模块被删除时,我会明确地“小心地”清除其 __dict__。(这始终在进行,只是以前没有“小心地”进行。)

动机

执行M1是因为这些变量是用户值常见的隐藏位置,并且它们在提议的顺序中出现得太晚了。(事实上,几乎所有报告的析构函数未按预期调用的问题都与这些变量有关。)

M3的存在是因为 __main__ 在概念上是程序的“根”——如果它没有被其他模块导入,它无论如何都会在M4步中首先被删除,但如果它**被**其他地方导入,删除 __main__ 是一种打破僵局的合理方式。

M4 是一个显式的垃圾回收循环——它删除所有没有被其他模块引用,只被模块表(sys.modules)本身引用的模块。然而,当存在相互导入时,它可能不会删除所有模块;剩余的步骤将处理这些情况。

M5 需要处理相互递归的导入,这些导入会创建循环,导致 M4 无法删除所有内容。

对 __builtin__ 和 sys 的特殊处理是因为它们被解释器通过许多操作隐式引用;__builtin__ 当然包含所有内置函数和异常;sys 包含标准 I/O 文件,这些文件被各种 I/O 操作隐式引用。所以它们在 M2 和 M4 中被排除。__builtin__ 最后被删除,因为它包含最基本和最核心的值。

模块字典清理的特殊处理是必要的,因为当模块定义 Python 函数或类时,会存在一个基本的循环引用。函数对象包含对其“全局”对象的引用,该对象是定义它的模块的 __dict__。由于 __dict__ 通常有一个对函数的引用,因此存在一个需要打破的循环,否则 __dict__ 将永远不会被垃圾回收。

请注意,基于引用计数的解决方案在一个模块内不起作用,因为函数之间的引用是按名称而不是按值进行的——两个相互递归的函数仍然可以都具有一个引用计数,因为它们相互执行名称查找。

C1 旨在为模块提供一种方式来定义在模块中其他任何东西之前被删除的全局变量。由于导入的模块或函数名称通常不以下划线开头,这意味着可以保证,当这些对象被删除时,它们可能需要的任何导入模块或函数仍然存在——当然,前提是对它们的唯一引用是在模块中。(此步骤已在发布的 1.5 版本中实现。)

C2 删除剩余对象,但保留“内部全局变量”__builtins__ 不动——这避免了 1.5 版本中存在的问题,例如在析构函数中使用“None”会引发 NameError!

C3 从模块表中删除对模块的引用,从而使稍后导入同一模块的操作失败。(用户代码可以删除此条目并仍然启动一个全新的导入——但如果他们如此聪明,那么他们活该。)

问题和疑问

P1. 当模块 M 的所有使用形式都是 ``from M import ...'' 时,模块 M 的引用计数将为 1。因此它将在 M1 步骤中被删除。这使得模块中除最微不足道(可能仍被其他模块引用)的函数之外的所有函数都变得无用,因为它们可能需要的导入模块和函数都已从它们的全局变量中删除。当然,一个简单的补救措施是不使用 ``from M import ...'',但这听起来可能会成为一个常见问题……问题是我不知道更好的方法——由于函数及其模块的 __dict__ 之间存在循环引用,我无法在 M1 步骤中使用 __dict__ 的引用计数。我认为这是可以接受的——这种行为在 1.4 及更早版本中也存在。(曾有人建议在导入模块中添加一个名为例如 ".module" 的引用,以表明依赖关系并防止此问题;虽然这可能很好地完成任务,但我犹豫不决地实现它,因为它可能会混淆内省工具。)