模块清理的改进建议
模块清理的改进建议
我正在尝试一种更好的方法来在执行运行结束时进行清理。在不实现真正的 GC 的情况下,我永远无法 100% 正确地完成它,但我可以基于实际观察实现一组可预测的规则,这将解决实际观察到的大部分问题。
这是我的建议。在本消息末尾,我列出了一些该建议的潜在问题并征求反馈。这可能会在 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
- 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 函数或类时,总是存在基本的循环引用。函数对象包含对函数“globals”对象的引用,该对象是定义它的模块的 __dict__。由于 __dict__ 通常有一个对函数的引用,因此存在一个需要打破的循环,否则 __dict__ 将永远不会被垃圾回收。
请注意,基于引用计数的解决方案在一个模块内不起作用,因为函数之间的引用是按名称而不是按值进行的——两个相互递归的函数仍然可以都具有引用计数 1,因为它们会互相进行名称查找。
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” 的引用,以指示依赖关系并防止此问题;虽然这可能会很好地完成工作,但我不太愿意实现它,因为它可能会使自省工具感到困惑。)