根据引用计数按"wrong"顺序调用的 Python 析构函数



据我所知,当对象的引用计数达到0时,应该调用Python析构函数。但这种假设似乎并不正确。查看以下代码:

class A:
def __init__(self, b):
self.__b = b
print("Construct A")

def __del__(self):
# It seems that the destructor of B is called here.
print("Delete A")
# But it should be called here

class B:
def __init__(self):
print("Construct B")

def __del__(self):
print("Delete B")

b = B()
a = A(b)

输出

Construct B                                                                                                                                                                                                                                   
Construct A                                                                                                                                                                                                                                   
Delete B                                                                                                                                                                                                                                      
Delete A

但是A引用了B,所以我希望得到以下输出:

Construct B                                                                                                                                                                                                                                   
Construct A                                                                                                                                                                                                                                   
Delete A                                                                                                                                                                                                                                      
Delete B

我没有得到什么?

因此,由于解释器关闭时对象仍然活动,因此实际上甚至不能保证__del__会被调用。在这一点上,该语言不能保证何时调用终结器。

来自文档:

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

注意,如果您将脚本更改为:

(py38) 173-11-109-137-SFBA:~ juan$ cat test.py
class A:
def __init__(self, b):
self.__b = b
print("Construct A")
def __del__(self):
# It seems that the destructor of B is called here.
print("Delete A")
# But it should be called here
class B:
def __init__(self):
print("Construct B")
def __del__(self):
print("Delete B")
b = B()
a = A(b)
del a
del b

然后,执行:

(py38) 173-11-109-137-SFBA:~ juan$ python test.py
Construct B
Construct A
Delete A
Delete B

虽然del不会删除对象,但它会删除引用,因此在解释器仍在运行时,它会强制引用计数达到0,因此顺序与您预期的一样。

有时,根本不会调用__del__。一种常见的情况是创建的文件对象

f = open('test.txt')

在全球范围内有实时引用。如果没有显式关闭,它可能不会调用__del__,文件也不会刷新,也不会写入任何内容。这是使用文件对象作为上下文管理器的一个很好的理由。。。

根据其他地方对此问题的评论,您可能不想使用__del__;它并不是C++意义上的析构函数。您可能希望将对象制作成上下文管理器(通过编写__enter____exit__方法(,并在with语句中使用它们,和/或为它们提供需要显式调用的close方法。

然而,要回答给定的问题:原因是这两个对象都有来自全局变量ab的引用;两个引用计数都不会变为零。当python解释器关闭并且正在收集所有非零计数对象时,将在最后调用析构函数。

要查看预期的行为,请将ab变量放入函数中,以便在执行的主要部分中引用计数为零。

class A:
def __init__(self, b):
self.__b = b
print("Construct A")
def __del__(self):
print("Delete A")
class B:
def __init__(self):
print("Construct B")
def __del__(self):
print("Delete B")
def foo():
b = B()
a = A(b)
foo()

在您缺少的东西中,有一个引用循环。大致为a->b->B->B.__init__->B.__init__.__globals__->a:

  • 您的A实例有一个对其__dict__的引用,该引用引用了您的B实例
  • 您的B实例具有对B类的引用
  • B类引用了它的__dict__,它引用了B的所有方法。(从技术上讲,如果你自己尝试访问B.__dict__,你会得到一个映射代理来包装B的"真正的__dict__"。B引用了真正的dict,而不是代理。(
  • B的每个方法都引用了它们的全局变量dict
  • 全局变量dict引用了A实例(因为这个dict是a全局变量所在的地方(

在引用循环中回收对象时,无法保证__del__方法的执行顺序。

如果你不相信引用循环的存在,那么很容易证明这些引用的存在:

import gc
print(a.__dict__ in gc.get_referents(a))
print(b in gc.get_referents(a.__dict__))
print(B in gc.get_referents(b))
# this bypasses the mappingproxy
# never use this to modify a class's dict - you'll cause memory corruption
real_dict = next(d for d in gc.get_referents(B) if isinstance(d, dict))
print(B.__init__ in gc.get_referents(real_dict))
print(B.__init__.__globals__ in gc.get_referents(B.__init__))
print(a in gc.get_referents(B.__init__.__globals__))

所有这些print都打印True


除此之外,还有一些其他答案已经提出的相关问题。您的对象一直存活到解释器关闭,所以根本不能保证__del__会被调用。此外,__del__是终结器,而不是析构函数。它没有像C++这样的语言中实际的析构函数那样的保证。

这个概念被称为合成:

在组合中,如果父对象被破坏,则子对象也不存在。合成实际上是一种强大的聚合类型,有时被称为"死亡"关系。例如,一所房子可能由一个或多个房间组成。如果房子被摧毁,那么作为房子一部分的所有房间也会被摧毁

对于要删除的对象,需要首先删除其所有属性。从上面的例子来看,房子的房间将首先被摧毁,以便房子被摧毁。

因此,A具有对象B的属性,该属性将首先被删除。

最新更新