Python 模块曾经被垃圾回收过吗?

Are Python modules ever garbage collected?

如果我在 Python 中加载一个模块,它会被垃圾回收吗?构建这个问题的另一种方式是,Python 在哪里保存对 Python 模块的引用?正如我假设的那样,如果不再有任何引用,垃圾收集器将删除一个模块。

这是我在 Python 解释器中尝试的一个例子:

>>> from importlib import import_module
>>> import sys
>>> import gc

>>> x = import_module('math')
>>> 'math' in sys.modules

这输出:

True

所以让我们删除脚本中对模块的引用。

>>> del x
>>> gc.collect()
>>> 'math' in sys.modules

Python 仍然跟踪数学模块,因为输出仍然是:

True

但现在如果我从 sys.modules 中删除数学,我将不再知道任何进一步的引用:

>>> del sys.modules['math']
>>> gc.collect()

然而,gc.collect()的输出是:

0

没有垃圾回收,所以模块不再在 sys.modules 或我的脚本中。为什么它没有被垃圾收集?

总的来说,至少在3.4及以后的版本中,模块对象在这方面应该没有什么特别之处。当然通常 sys.modules 中每个加载的模块都有一个引用,但如果你明确删除了它,一个模块应该能够消失。

话虽这么说,过去肯定存在一些问题,在某些情况下会阻止这种情况的发生,而且我不保证 3.7 之后不会再有任何此类问题。

不幸的是,您的测试实际上并没有测试任何东西。大概你正在使用 CPython。在 CPython 中,垃圾收集器使用引用计数——它直接在每个对象上存储一个计数,每当一个新名称绑定到它时递增和递减计数,如果计数变为 0 则立即删除它。 gc 模块中的东西是一个循环收集器,需要它来处理一些特殊情况,其中两个(或多个)对象相互引用但没有其他人引用它们。如果模块不是这样一个循环的一部分,它会在你调用 gc.collect() 之前被删除,所以当然会 return 0。但是那个 0 什么也没有告诉你。

您的测试还有其他问题。

首先,您不应该在交互式解释器中测试垃圾。各种额外的东西都以难以解释的方式保存在那里。写个测试脚本就好多了

其次,你不应该使用 math 作为你的测试。它是一个扩展模块(也就是说,用 C 而不是 Python 编写的),即使在 3.5 中进行了重大更改之后,它们仍然无法正常工作。它也是一个核心模块,可能是启动的一部分或解释器的其他部分需要的,即使您没有从您的代码中引用它。所以,最好使用其他东西。

无论如何,我认为可能有一种方法可以直接测试它,而无需使用调试器,但无法保证它是否会起作用。

首先,您需要创建 types.ModuleType 的子类,它有一个 __del__ 方法来打印一些消息。然后,您只需要导入一个模块(一个 .py 模块,而不是扩展模块)并将其 __class__ 设置为该子类。这可能与 .py 文件中的 __class__ = MyModuleSubclass 一样简单。现在,当它被收集时,它的析构函数将 运行,并且你将得到它被收集的证据。 (好吧,除非析构函数恢复它,否则证明它已被收集,但如果你的析构函数除了打印静态字符串之外什么都不做,希望这不是一个担心。)

根据 abarnert 的回答,我创建了以下 run-it-yourself 示例来演示我试图理解的行为:

from types import ModuleType
from importlib import import_module
import sys

class MyModule(ModuleType):
    def __del__(self):
        print('I am being deleted')

if __name__ == '__main__':
    x = import_module('urllib3')
    x.__class__ = MyModule
    del x
    del sys.modules['urllib3'] # Comment this out and urllib3 will NOT be garbage collected before the script finishes
    print('finishing')

当运行原样输出:

I am being deleted

finishing

注释掉 del sys.modules['urllib3'] 行的输出:

finishing

I am being deleted

很明显,当对模块的所有引用都被删除时,模块会像预期的那样被垃圾回收,除非所讨论的模块有些特殊,否则当应用程序和 sys.modules 已被删除。