据我所知,当对象的引用计数达到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
方法。
然而,要回答给定的问题:原因是这两个对象都有来自全局变量a
和b
的引用;两个引用计数都不会变为零。当python解释器关闭并且正在收集所有非零计数对象时,将在最后调用析构函数。
要查看预期的行为,请将a
和b
变量放入函数中,以便在执行的主要部分中引用计数为零。
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
的属性,该属性将首先被删除。