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

WKP*_*lus 11 python python-2.x python-3.x python-internals

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

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

    def __del__(self):
        print "A del"

class B(object):
    a = A()
Run Code Online (Sandbox Code Playgroud)

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

A init
Run Code Online (Sandbox Code Playgroud)

但是,如果我更改class B(object)class B(),我会得到:

A init
A del
Run Code Online (Sandbox Code Playgroud)

我在__del__ doc中找到了一个注释:

无法保证为解释器退出时仍然存在的对象调用del()方法.

然后,我想这是因为当解释器存在时B.a仍然引用(由类引用B).

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

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

我在这里发现了类似的问题,但我认为答案还不够明确.

Ant*_*ala 11

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

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


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

>>> class A(object):
...     pass
>>> A.__mro__[0] is A
True
Run Code Online (Sandbox Code Playgroud)

这意味着它们不能立即删除*,但仅在运行垃圾收集器时才会删除.由于主模块正在保存对它们的引用,因此它们将保留在内存中,直到解释器关闭.最后,在模块清理期间,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()
Run Code Online (Sandbox Code Playgroud)

B上面的新式类一样,输出是

A init
About to execute gc.collect()
A del
Run Code Online (Sandbox Code Playgroud)

使用B旧式class(class B:),输出是

A init
A del
About to execute gc.collect()
Run Code Online (Sandbox Code Playgroud)

也就是说,gc.collect()即使最后一次外部引用已被删除,新样式类也被删除了; 但旧式的课程立刻被删除了.


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

A init
A del
Run Code Online (Sandbox Code Playgroud)

而Python <= 3.3则会打印出来

A init
Run Code Online (Sandbox Code Playgroud)

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