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


我找到了一些有趣的东西,这里有一段代码:

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

不能保证 德尔()方法被称为对象   当解释器退出时仍然存在。

然后,我猜是因为那个 B.a 仍被引用(由类引用 B)当翻译存在时。

所以,我添加了一个 del B 在解释器手动存在之前,然后我找到了 a.__del__() 被称为。

现在,我对此感到有些困惑。为什么是 a.__del__() 使用旧样式时调用?为什么新旧风格的行为有不同的行为?

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


11256
2018-04-08 09:50


起源

是否 stackoverflow.com/questions/14323734/... 回答你的问题? - 101
有趣!重现和散列随机化不会影响它。我怀疑当模块被拆除时,仍然可能存在对新式课程的引用。 - Martijn Pieters♦
@MJeffryes:解释器退出时清除模块。所以在这里运行意味着将此代码作为脚本运行。拆除模块的解释器出口(将其中的所有引用设置为None)从而触发模块 A.__del__ 调用实例也被清除。 - Martijn Pieters♦
@figs对新/旧样式类之间的区别也没有答案 - WKPlus
@WKPlus:问题是一样的;虽然mgilson是正确的,但可能有一点不同,但这种差异是一个实现细节。 - Martijn Pieters♦


答案:


TL; DR:这是一个 老问题 在CPython中,最终修复了 CPython 3.4。由模块全局变量引用的引用循环保持活动的对象在3.4之前的CPython版本中的解释器出口上未正确完成。新式类在其中具有隐式循环 type 实例;旧式班(类型 classobj)没有隐式参考周期。

即使在这种情况下修复,CPy​​thon 3.4文档仍然建议不要依赖 __del__ 在翻译出口被召唤 - 考虑自己被警告。


新风格类本身具有参考周期:最值得注意的是

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

这意味着它们不能立即删除*,但仅限于运行垃圾收集器时。由于主模块对它们的引用,它们将保留在内存中,直到解释器关闭。最后,在模块清理期间,main中的所有模块全局名称都设置为指向 None,以及任何对象的引用计数减少到零(例如旧式类)也被删除。但是,具有引用周期的新式类将不会由此发布/最终确定。

循环垃圾收集器不会在解释器出口处运行(这是由允许的 CPython文档

不能保证 __del__() 对于在解释器退出时仍然存在的对象调用方法。


现在,Python 2中的旧式类没有隐式循环。当CPython模块清理/关闭代码将全局变量设置为 None,唯一剩下的对类的引用 B 掉了;然后 B 被删除,最后一次引用 a 被丢弃了 a 也是最终确定的。


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

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 作为上面的新式类,输出是

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

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

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

也就是说,新风格的类只在之后被删除了 gc.collect() 即使最后一次外部参考已被删除;但旧式的课程立刻被删除了。


其中大部分已经存在 固定 在 Python 3.4: 谢谢 PEP 442,其中包括 基于GC代码的模块关闭程序。现在即使在解释器退出时,模块全局变量也使用普通垃圾收集来完成。如果在Python 3.4下运行程序,程序将打印出来

A init
A del

而Python <= 3.3将打印

A init

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


10
2018-04-08 15:45