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

Python destructor called in the "wrong" order based on reference count

据我了解Python当一个对象的引用计数达到0时应该调用析构函数。但是这个假设似乎不正确。看下面的代码:

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

我没有得到什么?

这个概念叫做composition:

In composition, if the parent object is destroyed, then the child objects also cease to exist. Composition is actually a strong type of aggregation and is sometimes referred to as a “death” relationship. As an example, a house may be composed of one or more rooms. If the house is destroyed, then all of the rooms that are part of the house are also destroyed.

删除一个对象,需要先删除它的所有属性。从上面的例子来看,房子的房间会先被摧毁,然后房子才会被摧毁。

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

根据其他地方关于此问题的评论,您可能不想使用 __del__;它并不是真正的 C++ 意义上的析构函数。您可能希望将对象变成上下文管理器(通过编写 __enter____exit__ 方法)并在 with 语句中使用它们,and/or 给它们 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()

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

From the docs:

It is not guaranteed that __del__() methods are called for objects that still exist when the interpreter exits.

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

(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__ 并且文件不会刷新并且您不会写入任何内容。这是将文件对象用作上下文管理器的重要原因...

在您遗漏的内容中,有一个引用循环。它大致 a->b->B->B.__init__->B.__init__.__globals__->a:

  • 您的 A 实例引用了它的 __dict__,后者引用了您的 B 实例。
  • 您的 B 实例引用了您的 B class。
  • 您的 B class 引用了它的 __dict__,它引用了 B 的所有方法。 (从技术上讲,如果您尝试自己访问 B.__dict__,您将得到一个映射代理包装 B 的“真实 __dict__”。B 具有对真实字典的引用,不是代理。)
  • B 的每个方法都引用了它们的全局变量字典。
  • 全局变量 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++ 等语言中所具有的相同类型的保证。