在Python中每个对象可以调用多少次`__del__`?

sco*_*awg 5 python garbage-collection destructor

我看到一些代码在__del__我发现好奇的对象上被明确调用,所以我试着用它来理解它是如何工作的.

我尝试了以下代码(我理解使用__del__它本身可能很糟糕,但我只想在这里尝试自己):

class A:
    def __init__(self, b):
        self.a = 123
        self.b = b
        print "a is {}".format(self.a)
    def __del__(self):
        self.a = -1
        print "a is {}".format(self.a)
        self.b.dont_gc_me = self
    def foo(self):
        self.a = 9999999
        print "a is {}".format(self.a)

class Foo:
    def __init__(self):
        self.a = 800
    def __del__(self):
        self.a = 0
Run Code Online (Sandbox Code Playgroud)

然后尝试(在IPython控制台中)以下内容:

In [92]: f = Foo()

In [93]: a = A(f)
a is 123

In [94]: a = None
a is -1

In [95]: f.__dict__
Out[95]: {'a': 800, 'dont_gc_me': <__main__.A instance at 0x2456830>}

In [96]: f = None

In [97]: 
Run Code Online (Sandbox Code Playgroud)

我看到__del__只调用一次,即使实例a A已被实例f保持活着Foo,当我将后者设置None为时,我也看不到第二次调用析构函数.

这里Python文档说:

请注意,该方法可以(尽管不推荐!)__del__() 通过创建对它的新引用来推迟实例的销毁.然后可以在稍后删除该新引用时调用它.无法保证__del__()在解释器退出时仍然存在的对象调用方法.

这似乎意味着__del__可以再次调用该方法,但不能保证它会.所以我的问题是:是否有某些情况__del__会被再次调用?(我认为设置fNone上面就可以了,但事实并非如此).还有其他值得注意的细微差别吗?

Tim*_*ers 6

这是一种方式:

xs = []
n = 0

class A:
    def __del__(self):
        global n
        n += 1
        print("time {} calling __del__".format(n))
        xs.append(self)

print("creating an A immediately thrown away")
A()
for _ in range(5):
    print("popping from xs")
    xs.pop()
Run Code Online (Sandbox Code Playgroud)

打印:

creating an A immediately thrown away
time 1 calling __del__
popping from xs
time 2 calling __del__
popping from xs
time 3 calling __del__
popping from xs
time 4 calling __del__
popping from xs
time 5 calling __del__
popping from xs
time 6 calling __del__
Run Code Online (Sandbox Code Playgroud)

因此,简而言之,__del__可以调用的次数没有限制.但不要依赖于此 - 语言最终可能会改变"规则".

循环使情况复杂化,因为当循环完全是垃圾时,没有可预测的顺序,属于循环的对象将被销毁.因为它一个周期,每一个对象是从周期每隔一个对象可到达的,因此执行__del__用于循环中的一个对象引用一个已经被破坏的对象.那里的主要问题是,CPython只是拒绝收集一个循环,其中至少有一个对象有一个__del__方法.

但是,如果一个"悬挂"垃圾循环的对象有一个__del__方法,如果后一个对象本身不在垃圾循环中,则没有问题.例:

class A:
    def __del__(self):
        print("A is going away")

class C:
    def __init__(self):
        self.self = self
        self.a = A()
Run Code Online (Sandbox Code Playgroud)

然后:

>>> c = C()
>>> import gc
>>> gc.collect()  # nothing happens
0
>>> c = None  # now c is in a trash self-cycle
>>> gc.collect()  # c.a.__del__ *is* called
A is going away
2
Run Code Online (Sandbox Code Playgroud)

这就是这个故事的寓意:如果你有一个"需要"运行析构函数的对象,但可能处于一个循环中,则将其__del__放入原始对象引用的简单对象中.像这样:

class CriticalResource:
    def __init__(self, resource):
        self.r = resource

    def __del__(self):
        self.r.close_nicely()  # whatever

class FancyObject:
    def __init__(self):
        # ...
        self.r = CriticalResource(get_some_resource())
        # ...
Run Code Online (Sandbox Code Playgroud)

现在a FancyObject可以在你喜欢的循环中.当它成为垃圾桶,周期不会阻止CriticalResource__del__被调用.

从Python 3.4开始

正如@delnan在评论中指出的那样,PEP 442改变了从Python 3.4开始的CPython规则(但这不会被移植到任何以前的CPython版本中). __del__方法最多只执行一次(通过gc - 当然用户可以显式调用它们任意次),并且具有__del__方法的对象是否是循环垃圾的一部分将不再重要.

该实现将在循环垃圾中找到的所有对象上运行所有终结器,并在记录其终结器已运行的每个此类对象上设置一个位.它在任何对象被拆除之前执行此操作,因此没有终结器可以在疯狂(部分或完全破坏)状态下访问对象.

实现的缺点是终结器可以以任意方式更改对象图,因此当前循环垃圾收集("gc")运行必须放弃循环垃圾中的任何内容再次可达("复活").这就是为什么终结者被允许最多运行一次的原因:否则gc可能被激活为永远不会通过在循环垃圾中复活死对象的终结者取得进展.

实际上,从3.4开始,CPython对__del__方法的处理将很像Java对终结器的处理.

  • 未来就在这里.CPython刚改变了规则:PEP 442说"遵循这个方案,对象的终结器总是被调用一次,即使它之后被复活了." 我说CPython的,因为我不认为它曾经是一个语言规则:替代实现(当然PyPy,Jython的可能和IronPython太)已经很长一段时间的工作方式不同,可能是因为他们的开始. (3认同)