【问题标题】:How many times can `__del__` be called per object in Python?Python中每个对象可以调用多少次`__del__`?
【发布时间】:2013-09-29 01:40:08
【问题描述】:

我看到了一些代码,其中__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]: 

我看到 __del__ 只被调用一次,即使 A 的实例 a 已被 Foo 的实例 f 保持活动状态,当我将后者设置为 None 时,我没有看到第二次调用析构函数。

Python docs here 说:

请注意,__del__() 是可能的(尽管不推荐!) 通过创建一个新的实例来推迟销毁实例的方法 参考它。然后可以在以后调用这个新的 引用被删除。不保证 __del__() 方法是 调用解释器退出时仍然存在的对象。

这似乎暗示__del__方法可能会被再次调用,尽管不能保证它会被再次调用。所以我的问题是:在某些情况下__del__ 会再次被调用吗? (我认为将f 设置为None 可以做到这一点,但事实并非如此)。还有其他值得注意的细微差别吗?

【问题讨论】:

    标签: python garbage-collection destructor


    【解决方案1】:

    这是一种方法:

    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 changes the CPython rules 从 Python 3.4 开始(但这不会向后移植到任何以前的 CPython 版本)。 __del__ 方法将最多执行一次(通过 gc - 当然用户可以显式调用它们任意次数),并且具有 __del__ 方法的对象是否是循环垃圾的一部分将不再重要。

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

    实现的缺点是终结器可以以任意方式更改对象图,因此如果循环垃圾中的任何内容再次变得可访问(“复活”),当前的循环垃圾收集(“gc”)运行必须放弃)。这就是为什么(将)允许终结器最多运行一次的原因:否则,在循环垃圾中复活死对象的终结器可能会导致 gc 永远不会取得进展。

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

    【讨论】:

    • 未来已来。 CPython 刚刚更改了规则:PEP 442 说“按照这个方案,一个对象的终结器总是被调用一次,即使它后来被复活了。”。我说 CPython 是因为我认为它从来都不是语言规则:替代实现(当然是 PyPy,也可能是 Jython 和 IronPython)在很长一段时间内一直以不同的方式工作,可能是从它们一开始。
    • 谢谢!我也在回答中说“CPython”;-) Here's a link to the PEP。请注意,这是为 Python 3.4 实现的,尚未发布。它不会被反向移植到任何早期的 CPython 行(Python 2 行或 Python 3.0、3.1、3.2 或 3.3 中没有)。因此,“旧”行为将在未来几年继续困扰人们:-(
    【解决方案2】:

    __del__ 永远不会为循环垃圾收集器收集的对象调用。

    由于在第一次调用 __del__ 时您正在创建一个循环,因此您将依赖循环收集器来清理

    再次调用__del__ 的唯一方法是手动中断循环引用

    【讨论】:

      【解决方案3】:

      Tim 的回答是权威的:终结器,__del__() 或其他(例如,生成器函数中的finally 子句),由于object resurrection,可以任意频繁地调用,这取决于实现和版本。然而,从 CPython 3.4 开始,终结器只被调用一次,每个 PEP 442 -- Safe object finalization,就像 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 Issue18214) 中,模块全局设置为 None,在改进完成后修复,and None 确保在交互模式下没有将引用存储在 _(最后一个表达式的值)中。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2019-02-08
        • 2014-10-20
        • 2016-03-01
        • 1970-01-01
        • 2013-06-13
        • 2010-09-27
        • 1970-01-01
        相关资源
        最近更新 更多