为什么新样式 class 和旧样式 class 在这种情况下有不同的行为?

Why do new style class and old style class have different behavior in this case?

我发现了一些有趣的东西,这里是一段代码:

class A(object):
    def __init__(self):
        print "A init"

    def __del__(self):
        print "A del"

class B(object):
    a = A()

如果我运行这个代码,我会得到:

A init

但如果我将 class B(object) 更改为 class B(),我将得到:

A init
A del

我在 __del__ doc 中发现了一条注释:

It is not guaranteed that del() methods are called for objects that still exist when the interpreter exits.

那么,我猜是因为当解释器存在时B.a仍然被引用(被class B引用)。

于是,我在解释器存在之前手动添加了一个del B,然后我发现a.__del__()被调用了。

现在,我对此有点困惑。为什么在使用旧样式 class 时调用 a.__del__()?为什么新旧样式 classes 有不同的行为?

我发现了一个类似的问题here,但我认为答案不够明确。

TL;DR:这是一个 old issue in CPython, that was finally fixed in CPython 3.4。在 3.4 之前的 CPython 版本中,由模块全局引用的引用循环保持活动的对象在解释器退出时未正确完成。新式 classes 在它们的 type 实例中有隐式循环;旧式 classes(classobj 类型)没有隐式引用循环。

即使在这种情况下已修复,CPython 3.4 文档仍然建议不要依赖 __del__ 在解释器退出时被调用 - 考虑自己警告。


新风格 classes 本身具有参考循环:最值得注意的是

>>> class A(object):
...     pass
>>> A.__mro__[0] is A
True

这意味着它们不能被立即删除*,只有当垃圾收集器是 运行 时才会被删除。由于对它们的引用由主模块持有,因此它们将保留在内存中,直到解释器关闭。最后,在模块清理期间,main 中的所有模块全局名称都设置为指向 None,并且无论哪个对象的引用计数都减少到零(您的旧式 class例如)也被删除。但是,具有引用循环的新式classes,不会是released/finalized。

循环垃圾收集器不会 运行 在解释器出口处(CPython documentation:

It is not guaranteed that __del__() methods are called for objects that still exist when the interpreter exits.


现在,Python2 中的旧式 classes 没有隐式循环。当 CPython 模块 cleanup/shutdown 代码将全局变量设置为 None 时,唯一剩余的对 class B 的引用被丢弃;然后 B 被删除,最后对 a 的引用被删除,a 也被最终确定。


为了证明新式 classes 有循环并需要 GC 扫描,而旧式 classes 没有,您可以在 C 中尝试以下程序Python 2(CPython 3 不再有旧式 classes):

import gc
class A(object):
    def __init__(self):
        print("A init")

    def __del__(self):
        print("A del")

class B(object):
    a = A()

del B
print("About to execute gc.collect()")
gc.collect()

B为新式class如上,输出为

A init
About to execute gc.collect()
A del

B作为旧式class(class B:),输出为

A init
A del
About to execute gc.collect()

也就是说,新样式 class 仅在 gc.collect() 之后被删除,即使对它的最后一个外部引用已经被删除;但旧式 class 立即被删除。


其中大部分已经 fixed in Python 3.4: thanks to PEP 442, which included the module shutdown procedure based on GC code。现在,即使在解释器退出时,模块全局变量也会使用普通垃圾收集完成。如果你 运行 你的程序在 Python 3.4 下,程序将打印

A init
A del

而 Python <=3.3 它将打印

A init

(请注意此时其他实现仍然可能会或可能不会执行__del__,无论它们的版本如何,等于或低于 3.4)