在Python中,每个对象的`__del__`方法最多可被调用一次。

5

我看到一些代码在对象上显式调用了__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

然后我在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]: 

我发现,即使实例a的引用由Foo的实例f保持,__del__方法也只被调用一次;当我将后者设置为None时,我没有看到析构函数第二次被调用。
Python文档中提到:
注意,__del__() 方法可以通过创建对实例的新引用来延迟实例的销毁(虽然不建议这样做!)。在删除此新引用时,可能会在以后的某个时间调用它。不能保证在解释器退出时仍存在的对象会调用__del__()方法。
这似乎意味着__del__方法可能会被再次调用,但并不保证。那么我的问题是:是否存在某种情况,__del__会被再次调用?(我认为上面将f设置为None会这样做,但事实并非如此)。还有其他值得注意的细节吗?
3个回答

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()

那将打印:

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__

简而言之,__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()

然后:

>>> 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

所以这个故事的寓意是:如果你有一个需要运行析构函数但可能处于循环中的对象,请将__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())
        # ...

现在一个FancyObject可以在任意数量的循环中。当它变成垃圾时,循环不会阻止调用CriticalResource__del__
从Python 3.4开始
正如@delnan在评论中指出的那样,PEP 442更改了CPython规则,从Python 3.4开始执行__del__方法将最多只执行一次(当然用户可以显式地调用它们任意次数),并且拥有__del__方法的对象是否是循环垃圾将不再重要。
该实现将运行所有发现在循环垃圾中的对象的终结器,并设置每个这样的对象上的位记录其终结器已经运行。它在任何对象被拆除之前完成此操作,因此没有终结器能够访问处于疯狂状态(部分或全部销毁)的对象。
实现的缺点是终结器可以以任意方式更改对象图,因此当前循环垃圾收集(“gc”)运行必须放弃,如果循环垃圾中的任何内容再次变得可达(“复活”)。这就是为什么最多允许终结器运行一次的原因:否则,由于终结器复活了循环垃圾中的死对象,gc可能会被激发而永远无法取得进展。 实际上,从3.4开始,CPython对__del__方法的处理方式将与Java对终结器的处理方式非常相似。

3
未来已经到来。CPython 刚刚改变了规则:PEP 442 表示“根据这个方案,一个对象的终结器总是被准确地调用一次,即使它后来被复活。”我说 CPython 是因为我不认为它曾经是一条语言规则:替代实现(特别是 PyPy,可能还包括 Jython 和 IronPython)长期以来一直在以不同的方式工作,可能自它们问世以来就如此。 - user395760
1
谢谢!我在我的回答中也提到了“CPython”;-) 这是PEP的链接。请注意,这仅适用于Python 3.4,尚未发布。它不会被移植到任何早期的CPython版本(Python 2系列或Python 3.0、3.1、3.2或3.3)。因此,“旧”的行为将继续让人困惑多年 :-( - Tim Peters

1

__del__方法不会被循环垃圾回收机制调用,因为它只能处理非循环引用的对象。

如果在第一次调用__del__方法时创建了一个循环引用,那么你就必须依赖于循环引用的收集器来清理对象。

唯一可以再次调用__del__方法的方式是手动断开循环引用。


0

Tim的回答是权威的:finalizers,__del__()或其他方式(例如生成器函数中的finally子句)可以由于对象复活而被任意调用,这取决于实现和版本。然而,从CPython 3.4开始,finalizers仅会被调用一次,根据PEP 442 -- 安全对象终结,就像Java一样,感谢Antoine Pitrou

然而,这需要一个Pythonic的例子。下面是一个:

import sys
cart = []
class PlagueVictim:
    RETORTS = ("I'm not dead.",
               "I'm getting better.",
               'I feel fine.',
               "I think I'll go for a walk.",
               'I feel happy. I feel happy.')
    DEFAULT_RETORT = "I'm still not dead."
    @classmethod
    def retort(cls, i):
        try: return cls.RETORTS[i]
        except IndexError: return cls.DEFAULT_RETORT
    def __init__(self): self.incarnation = 0
    def __del__(self):
        print(self.retort(self.incarnation))
        if cart is not None: cart.append(self)
        self.incarnation += 1

cart.append(PlagueVictim())
print("> Here's one.")
cart.pop() and None
if not cart: sys.exit()

print(">> 'ere, he says he's not dead.")
# Stubborn, aren't you?
cart.pop() and None
cart.pop() and None
cart.pop() and None
cart.pop() and None
del PlagueVictim.__del__
print('*thwack*')
cart.pop() and None
print('>> Ah, thank you very much.')
print(len(cart))

细心的读者会注意到None检查,这是因为在CPython <3.4(Issue18214)中模块全局变量被设置为None,随着改进的最终化得到修复,并且and None确保在交互模式下不会将引用存储在_(最后一个表达式的值)中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接