Python中的__del__()方法有什么用?

15

来自Python文档

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

据我所知,也没有办法保证一个对象在解释器退出前停止存在,因为由垃圾回收器决定对象删除的时间和方式。

那么,这个方法有什么意义呢?你可以在其中编写清理代码,但无法保证它会被执行。

虽然可以使用try-finallywith子句来解决此问题,但我仍然想知道__del__()方法的一个有意义的应用场景是什么。


1
__del__ 在 Python 纯粹是基于引用计数的早期,更有意义。 - user2357112
3
在长时间运行的进程中(例如服务器),进行清理工作也是有道理的。 - Klaus D.
1
__del__ 保证在实例即将被销毁时被调用。你引用的是解释器退出时销毁不被保证的情况。这是一个不同的故事,我认为在这种情况下 "try/finally" 或 "with" 并没有帮助。因此,__del__ 可以很有用,例如当程序员犯错误并忘记使用 "try/finally" 或 "with" 时,可以自动回收资源。 - freakish
这里有一个例子,虽然可能可以通过其他方式重复,正如你所说,在复杂的代码中它可能不会被调用。 - snakecharmerb
7个回答

4

它可以用于释放对象管理的资源:https://github.com/python/cpython/blob/master/Lib/zipfile.py#L1805

如文档字符串中所述,这是一种“最后的手段”,因为只有在gc运行时对象才会被关闭。

正如您在问题中提到的,首选方法是自己调用close,可以直接调用.close()或使用上下文管理器with Zipfile() as z:


3
阅读了所有这些答案——没有一个令我满意地回答了我所有的问题/疑虑——并重新阅读了Python文档,我得出了自己的结论。以下是我对此问题的思考总结。

不依赖具体实现

您引用的来自{{link1:__del__方法文档}}的段落如下:

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

但是,不仅不能保证在解释器退出期间销毁的对象会调用__del__(),甚至在正常执行期间也不能保证对象被垃圾回收,{{link2:来自Python语言参考手册中的“数据模型”部分}}:

对象从未被显式地销毁;然而,当它们变得不可达时,它们可能会被垃圾回收。允许实现推迟垃圾回收或完全省略垃圾回收 - 垃圾回收的实现质量是如何实现的问题,只要不收集仍然可达的对象。

因此,回答您的问题:

“那么,拥有这个方法的意义在哪里呢?你可以在其中编写清理代码,但不能保证它会被执行。”从一个与实现无关的角度来看,__del__ 方法有什么用途,作为代码的基本组成部分可以依赖吗?“没有。完全没有。”从这个角度来看,它基本上是无用的。
尽管如此,从实际的角度来看,正如其他答案所指出的那样,您可以使用 __del__ 作为最后的手段机制来(尝试)确保在对象被销毁之前执行任何必要的清理,例如释放资源,如果用户忘记显式调用 close 方法。这不是一种救命稻草,而是一种“即使不能保证工作,增加额外的安全机制也不会有害”,实际上,大多数 Python 实现大多数时间都能捕获它。但不能依赖它。

特定于实现

话虽如此,如果您知道您的程序将在特定的Python实现集上运行,那么您可以依赖于垃圾回收的实现细节。例如,如果您使用CPython,则可以“依赖于”垃圾回收的事实,即在正常执行期间(即在解释器退出之外),如果非循环引用对象的引用计数达到零,它将被回收并调用其 __del__ 方法,就像其他答案所指出的一样。从以上同一小节中:

CPython实现细节:CPython目前使用一个带有(可选)延迟检测的循环链接垃圾收集方案,该方案能够尽快地收集大多数成为不可访问的对象,但无法保证收集包含循环引用的垃圾。

但是,这仍然非常不稳定,而且不应该过度依赖,因为正如上面提到的,它仅对不属于循环引用图的对象进行了保证。

其他实现行为不同,而CPython也可能会发生变化。当对象变得不可达时,不要依赖于立即最终的处理(因此您应该始终明确关闭文件)。


标题:

底线

从纯粹主义的角度来看,__del__方法完全没有用处。从稍微不那么纯粹主义的角度来看,它仍然几乎没有用处。从实际角度来看,它可能作为你代码的一种补充特性有用,但绝不是必要的


1
谢谢,我也开始得出同样的结论了。虽然我觉得这很遗憾,标准本来可以规定当程序退出时,必须首先调用所有剩余对象的__del __()方法。这样我们就可以像在C++中一样依赖它,而且就不需要(或者少需要)所有的with子句了。看起来实现起来很简单,但也许我错过了一些使它变得复杂的实现方面。 - PieterNuyts

1

它基本上被用于强制调用一个方法,该方法应在该对象的所有活动完成后调用一次,例如:

def __del__(self):
    self.my_func()

现在,您可以确信当对象工作完成后将调用my_func

运行此程序,您就会知道发生了什么。

class Employee: 
  
    def __init__(self, name): 
        self.name = name
        print('Employee created.') 
    
    def get_name(self):
        return self.name

    def close(self):
        print("Object closed")

    # destructor
    def __del__(self):
        self.close()
  
obj = Employee('John')

print(obj.get_name())

# lets try deleting the object!
obj.__del__() # you don't need to run this

print("Program ends")

print(obj.get_name())

输出

> Employee created.
> John 
> Object closed 
> Program ends  
> John 
> Object closed

1
我不明白为什么 John 会被再次打印?另外,我认为第二个 Object closed 的打印不是一定会发生的,因为标准规定:“当解释器退出时,不保证调用 del() 方法来处理仍然存在的对象。”我注意到这在某些 Python 实现中可以工作,但我认为您不能指望它。 - PieterNuyts

1

您说:

不能保证在解释器退出时仍存在的对象都会调用 del() 方法。

这是很真实的,但有许多情况下,对象被创建后,对这些对象的引用要么被明确地设置为 None 而“销毁”,要么超出了范围。这些对象,在创建时或在执行过程中分配资源。当用户完成对象使用后,应该调用一个 closecleanup 方法来释放那些资源。然而,最好还是有一个析构方法,即 __del__ 方法,在没有更多引用指向对象时被调用,以检查是否已调用了清理方法,如果没有,则调用自身进行清理。在可能不会在退出时调用 __del__ 的情况下,在这一点上重新获取资源可能并不太重要,因为程序正在被终止(当然,在 cleanupclose 执行更多的功能时,除了回收资源外,还要执行必要的终止函数,如关闭文件,那么依赖于在退出时调用 __del__ 就会变得有问题)。

重点是,在像CPython这样的引用计数实现中,当最后一个引用对象被销毁时,您可以依赖于__del__被调用:
import sys

class A:
    def __init__(self, x):
        self.x = x

    def __del__(self):
        print(f'A x={self.x} being destructed.')


a1 = A(1)
a2 = A(2)
a1 = None
# a1 is now destroyed
input('A(1) should have been destroyed by now ...')
a_list = [a2]
a_list.append(A(3))
a_list = None # A(3) should now be destroyed
input('A(3) should have been destroyed by now ...')
a4 = A(4)
sys.exit(0) # a2 and a4 may or may not be garbage collected

输出:

A x=1 being destructed.
A(1) should have been destroyed by now ...
A x=3 being destructed.
A(3) should have been destroyed by now ...
A x=2 being destructed.
A x=4 being destructed.

除了a2a4这两个对象,所有类A的其他实例都将被“销毁”,即删除。

实际用法是,例如调用函数bar创建一个B实例,该实例又会创建一个A实例。当函数bar返回B的引用和隐式销毁A时,如果A实例上的close方法尚未被调用,则将自动进行清理:

class A:
    def __init__(self, x):
        self.x = x
        self.cleanup_done = False
 
    def close(self):
        print(f'A x={self.x} being cleaned up.')
        self.cleanup_done = True
 
 
    def __del__(self):
        if not self.cleanup_done:
            self.close()
 
 
class B:
    def __init__(self, x):
        self.a = A(x)
 
    def foo(self):
        print("I am doing some work")
 
 
def bar():
    b = B(9)
    b.foo()
 
def other_function():
    pass
 
if __name__ == '__main__':
    bar()
    other_function()

Python演示

为了使B实例显式调用A实例的close方法,它必须实现自己的close方法,然后委托给A实例的close方法。但是,使用__del__方法来达到这个目的没有意义。因为如果那样工作,那么A实例自己的__del__方法就足以进行清理。


1
重点是它只适用于CPython实现,而不是Python语言本身。正如@Pieter所说,对象何时被垃圾回收完全取决于实现,甚至可能根本不进行垃圾回收:"实现可以延迟垃圾回收或完全省略..."。话虽如此,在实际层面上,我相信大多数流行的Python实现在对象引用计数达到0时会进行垃圾回收,至少在程序终止之外。 - Anakhand
2
@Anakhand 我肯定应该澄清一下,我关于能够依赖 __del__ 在没有更多引用时被调用的回答是指针对 CPython 的。但是,编写 __del__ 为类提供了额外的机会来进行清理,如果客户端忽略了调用 closecleanup 或你选择称之为清理例程的任何其他方法。这是我的回答的主要观点,正如我们所说的,“迟做总比不做好”,即使在某个实现中垃圾回收被延迟了(如果完全省略垃圾回收,你也不会比以前更糟)。 - Booboo
1
@Booboo 是的,我也决定采用那种解释;我认为那是对这个问题最好的概括。 - Anakhand
@Booboo 我认为注释“# A(2) and A(3) should now be destructed”是不正确的。我不明白为什么此时应该销毁A(2),因为它仍然被a2引用。你的演示确实说它被销毁了,但那可能只是在程序结束时(它也说a4被销毁)。如果我是对的,下面的文本也是错误的。只是想看看你是否同意在编辑你的答案之前。 - PieterNuyts
@PieterNuyts 当然,你是正确的——这是我的疏忽。我已经更新了代码,并在关键位置添加了一些"输入"语句,以证明对象在预期时已被销毁。感谢你指出这个问题。 - Booboo
显示剩余2条评论

0

我最近发现一个需要实现__del__的例子:

正如其他答案中提到的那样,在程序执行期间,无法保证__del__会在何时甚至是否被调用。然而,如果你把这个语句反过来看,这也意味着它有可能在某个时刻被调用。

在我的情况下,我有一个依赖于初始化期间创建的临时文件的类,例如:

import tempfile

class A:
    def __init__(self):
        self.temp_file = tempfile.NamedTemporaryFile()

然而,我也依赖于一个外部程序,它可能会删除该文件(例如,如果其内容无效)。在这种情况下,如果临时文件与A的实例一起被回收,将会抛出异常,因为文件不再存在。

因此,解决方案是尝试在__del__方法中关闭文件,并在那里捕获任何异常:

    def __del__(self):
        try:
            self.temp_file.close()
        except FileNotFoundError:
            # Do something here
            pass

因此,在这种情况下实现__del__,我可以指定如何处理资源,并在引发异常时采取什么措施。

但是如果您不实现 __del__(),它将什么也不会做,如果文件不存在,也不会抛出任何异常,对吧?那么这种解决方案比不实现 __del__() 更好在哪里呢?当然,通过实现它,您增加了文件在某个时刻被正确关闭的机会,但这并不是保证。 - PieterNuyts
如果您不实现它,当垃圾收集器决定删除您的实例时,您可能会遇到异常。例如,如果您运行以下代码,您将获得FileNotFoundError:https://pastebin.com/32F233w3。您需要使用`__del__`来捕获此异常并指定如何正确地摆脱此实例。尽管在这种情况下忽略了异常,因此您可能会认为这并不是什么大问题。 - Aratz

0

析构函数在对象被销毁时调用。在Python中,由于Python具有自动处理内存管理的垃圾回收器,因此不像C++那样需要使用析构函数。

__del__()方法在Python中被称为析构函数方法。当所有对该对象的引用都已删除即对象被垃圾回收时,将调用该方法。

析构函数声明的语法

def __del__(self):
  # body of destructor

注意:当对象失去引用或程序结束时,对象的引用也会被删除。

示例1:这是析构函数的简单示例。通过使用del关键字,我们删除了对象“obj”的所有引用,因此析构函数会自动调用。

    # Python program to illustrate destructor 
    class Employee: 
      
        # Initializing 
        def __init__(self): 
            print('Employee created.') 
      
        # Deleting (Calling destructor) 
        def __del__(self): 
            print('Destructor called, Employee deleted.') 
      
    obj = Employee() 
    del obj 

#Output
#Employee created.
#Destructor called, Employee deleted.

注意:析构函数在程序结束时或当所有对该对象的引用被删除时调用,即当引用计数变为零时,而不是在对象超出范围时。

示例2:此示例说明了上述注意事项。在这里,请注意,在打印“程序结束…”之后调用析构函数。

# Python program to illustrate destructor 
  
class Employee: 
  
    # Initializing  
    def __init__(self): 
        print('Employee created') 
  
    # Calling destructor 
    def __del__(self): 
        print("Destructor called") 
  
def Create_obj(): 
    print('Making Object...') 
    obj = Employee() 
    print('function end...') 
    return obj 
  
print('Calling Create_obj() function...') 
obj = Create_obj() 
print('Program End...') 

#Output:
#Calling Create_obj() function...
#Making Object...

示例3:现在,考虑以下示例:

# Python program to illustrate destructor 
  
class A: 
    def __init__(self, bb): 
        self.b = bb 
  
class B: 
    def __init__(self): 
        self.a = A(self) 
    def __del__(self): 
        print("die") 
  
def fun(): 
    b = B() 
  
fun() 

#Output:
#die

在这个例子中,当调用函数fun()时,它创建了类B的一个实例,并将自身传递给类A,然后类A设置对类B的引用,从而导致循环引用。
一般来说,Python的垃圾回收器用于检测这些类型的循环引用并将其删除,但在这个例子中,使用自定义析构函数将此项标记为“不可回收”。 简单地说,它不知道以何种顺序销毁对象,所以它将它们保留。因此,如果您的实例涉及循环引用,它们将在应用程序运行期间一直存在于内存中。
来源:Python中的析构函数

“当对象超出引用范围时,对该对象的引用也会被删除” - 你的意思是什么?我认为你想说的是作用域(即函数闭包)。 - Anakhand
好的,这里的“引用”是指对象名称所指向的位置。例如,在shivam = person()中,“shivam”是一个引用,指向内存中“person()”类的一个实例。 - Shivam Jha

-1

__del()__ 的作用类似于析构函数,有时会通过垃圾回收器自动调用(我说有时而不是总是,因为你永远不知道它何时运行),所以使用它有点无用而不是有用。


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