如何正确清理Python对象?


461
class Package:
    def __init__(self):
        self.files = []

    # ...

    def __del__(self):
        for file in self.files:
            os.unlink(file)

__del__(self)上面的失败,并带有AttributeError异常。我了解Python__del__()调用时不保证存在“全局变量”(在这种情况下是成员数据吗?)。如果是这种情况,并且这是导致异常的原因,那么如何确保对象正确销毁?


3
除非您正在谈论程序退出时的内容,否则在读取链接的内容后,全局变量消失似乎并不适用于此,在此期间,根据您链接的内容,我猜想os模块本身可能已经消失了。否则,我认为它不适用于__del __()方法中的成员变量。
凯文·安德森

3
在程序退出之前很久就抛出了异常。我得到的AttributeError异常是Python表示无法将self.files识别为Package的属性。我可能会错了,但是如果使用“全局变量”,它们并不意味着方法的全局变量(但可能是类的局部变量),则我不知道是什么导致了此异常。Google暗示Python保留在调用__del __(self)之前清除成员数据的权利。
威廉姆特尔2009年

1
发布的代码似乎对我有用(在Python 2.5中)。您能发布失败的实际代码吗?还是简化的代码(越简单的更好的版本仍会导致错误?)
Silverfish,2009年

@wilhelmtell您能举一个更具体的例子吗?在我所有的测试中,del析构函数都能完美运行。
未知

7
如果有人想知道:本文详细说明了为什么__del__不应该将其用作的对应内容__init__。(即,它是不是在这个意义上,一个“析构函数” __init__是一个构造函数。
富兰克林

Answers:


619

我建议使用Python的with语句来管理需要清理的资源。使用显式close()语句的问题在于,您必须担心人们会忘记完全调用它,或者忘记将其放在finally块中以防止发生异常时发生资源泄漏。

要使用该with语句,请使用以下方法创建一个类:

  def __enter__(self)
  def __exit__(self, exc_type, exc_value, traceback)

在上面的示例中,您将使用

class Package:
    def __init__(self):
        self.files = []

    def __enter__(self):
        return self

    # ...

    def __exit__(self, exc_type, exc_value, traceback):
        for file in self.files:
            os.unlink(file)

然后,当有人想使用您的课程时,他们将执行以下操作:

with Package() as package_obj:
    # use package_obj

变量package_obj将是Package类型的实例(它是__enter__方法返回的值)。__exit__无论是否发生异常,都会自动调用其方法。

您甚至可以进一步采用这种方法。在上面的示例中,仍然可以使用其构造函数实例化Package而无需使用该with子句。您不希望这种情况发生。您可以通过创建定义__enter____exit__方法的PackageResource类来解决此问题。然后,将严格在__enter__方法内部定义Package类并返回。这样,调用者永远无法在不使用with语句的情况下实例化Package类:

class PackageResource:
    def __enter__(self):
        class Package:
            ...
        self.package_obj = Package()
        return self.package_obj

    def __exit__(self, exc_type, exc_value, traceback):
        self.package_obj.cleanup()

您将按以下方式使用它:

with PackageResource() as package_obj:
    # use package_obj

35
从技术上讲,可以显式调用PackageResource().__ enter __(),从而创建一个永远不会完成的Package ...但是他们确实必须试图破坏代码。大概不用担心。
David Z

3
顺便说一句,如果您使用的是Python 2.5,则需要从以后的 import with_statement导入,以便能够使用with语句。
克林特·米勒

2
我找到了一篇文章,该文章有助于说明__del __()为什么如此行事,并为使用上下文管理器解决方案提供了可信度:andy-pearce.com/blog/posts/2013/Apr/python-destructor-drawbacks
eikonomega

2
如果您想传递参数,该如何使用漂亮而干净的结构?我希望能够做到with Resource(param1, param2) as r: # ...
snooze92 2014年

4
@ snooze92,您可以为Resource提供一个__init__方法,该方法可以将* args和** kwargs存储在self中,然后将它们传递给enter方法中的内部类。使用with语句时,在__enter__之前调用__init__
Brian Schlenker

48

标准方法是使用atexit.register

# package.py
import atexit
import os

class Package:
    def __init__(self):
        self.files = []
        atexit.register(self.cleanup)

    def cleanup(self):
        print("Running cleanup...")
        for file in self.files:
            print("Unlinking file: {}".format(file))
            # os.unlink(file)

但是您应该记住,这将持久化所有创建的实例,Package直到终止Python。

使用上面的代码的演示保存为package.py

$ python
>>> from package import *
>>> p = Package()
>>> q = Package()
>>> q.files = ['a', 'b', 'c']
>>> quit()
Running cleanup...
Unlinking file: a
Unlinking file: b
Unlinking file: c
Running cleanup...

2
关于atexit.register方法的好处是,您不必担心类用户的工作(他们使用了with吗?他们是否明确调用过__enter__吗?)如果您需要在python之前进行清理,那么当然是不利的一面。退出,它将无法正常工作。就我而言,我不在乎是在对象超出范围时还是在python退出之前不是。:)
hlongmore '18

我可以使用输入和退出以及添加atexit.register(self.__exit__)吗?
myradio

@myradio我不知道这有什么用?您不能执行里面的所有清除逻辑__exit__并使用contextmanager吗?另外,还__exit__需要接受其他参数(例如__exit__(self, type, value, traceback)),因此您需要对这些参数进行认证。无论哪种方式,听起来您都应该在SO上发布一个单独的问题,因为您的用例看起来很不正常?
ostrokach

33

作为克林特答案的附录,您可以简化PackageResource使用contextlib.contextmanager

@contextlib.contextmanager
def packageResource():
    class Package:
        ...
    package = Package()
    yield package
    package.cleanup()

另外,尽管可能不如Pythonic,但您可以重写Package.__new__

class Package(object):
    def __new__(cls, *args, **kwargs):
        @contextlib.contextmanager
        def packageResource():
            # adapt arguments if superclass takes some!
            package = super(Package, cls).__new__(cls)
            package.__init__(*args, **kwargs)
            yield package
            package.cleanup()

    def __init__(self, *args, **kwargs):
        ...

并简单地使用with Package(...) as package

为了使事情更短,请命名清理函数close并使用contextlib.closing,在这种情况下,您可以通过使用未修改的Package类,with contextlib.closing(Package(...))或者将其改写__new__为更简单的类。

class Package(object):
    def __new__(cls, *args, **kwargs):
        package = super(Package, cls).__new__(cls)
        package.__init__(*args, **kwargs)
        return contextlib.closing(package)

而且此构造函数是继承的,因此您可以简单地继承,例如

class SubPackage(Package):
    def close(self):
        pass

1
这太棒了。我特别喜欢最后一个例子。不幸的是,我们无法避免该方法的四行样板Package.__new__()也许我们可以。我们可能可以定义一个类修饰器或为我们通用化该样板的元类。Python思维的食物。
Cecil Curry

@CecilCurry谢谢,很好。任何继承自的类Package也应该这样做(尽管我还没有测试过),因此不需要元类。尽管我过去发现使用元类的一些非常奇怪的方法……
Tobias Kienzler

@CecilCurry实际上,构造函数是继承的,因此您可以使用Package(或更佳的名为的类Closing)作为类的父级,而不是object。但是,不要问我多重继承如何将其弄乱了……
Tobias Kienzler

17

我不认为实例成员可能在__del__调用之前被删除。我的猜测是您特定AttributeError的原因是其他原因(也许您在其他地方错误地删除了self.file)。

但是,正如其他人指出的那样,您应该避免使用__del__。这样做的主要原因是__del__不会对的实例进行垃圾回收(只有当其引用计数达到0时,它们才会被释放)。因此,如果您的实例涉及循环引用,则它们将在应用程序运行期间一直存在于内存中。(尽管我可能对所有这些都弄错了,我不得不再次阅读gc文档,但是我相当确定它的工作原理是这样的)。


5
与对象__del__可以被收集垃圾,如果与其他物体其引用计数__del__是零,并且它们是不可到达的。这意味着,如果在的对象之间有一个参考循环__del__,则不会收集任何参考循环。但是,任何其他情况都应按预期解决。
Collin 2013年

“从Python 3.4开始,__ del __()方法不再阻止垃圾回收参考周期,并且在解释器关闭期间不再将模块全局变量强制为None。因此,此代码应该在CPython上没有任何问题。” - docs.python.org/3.6/library/...
托马斯Gandor

14

更好的选择是使用weakref.finalize。请参见“ 终结器对象”和“ 将终结器与__del __()方法进行比较”中的示例。


1
今天使用此方法,它可以完美工作,比其他解决方案更好。我有一个基于多处理的通信器类,它打开一个串行端口,然后有一个stop()方法来关闭端口和join()进程。但是,如果程序意外退出,stop()则不会调用-我用终结器解决了这一问题。但是无论如何,我都调用_finalizer.detach()stop方法来防止两次调用(手动,然后由终结器再次调用)。
Bojan P.

3
海事组织,这确实是最好的答案。它结合了在垃圾收集时清理的可能性和在出口时清理的可能性。需要注意的是,python 2.7没有weakref.finalize。
hlongmore

12

我认为问题可能出在__init__如果代码比显示的更多?

__del__即使__init__未正确执行或引发异常也会被调用。

资源


2
听起来很有可能。使用时避免此问题的最佳方法__del__是在类级别上显式声明所有成员,以确保它们始终存在,即使__init__失败也是如此。在给定的示例中,files = ()虽然大多数情况下您只是分配None;无论哪种情况,您都仍然需要在中分配实际值__init__
索伦Løvborg

11

这是一个最小的工作框架:

class SkeletonFixture:

    def __init__(self):
        pass

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    def method(self):
        pass


with SkeletonFixture() as fixture:
    fixture.method()

重要提示:自我回报


如果您像我一样,并且忽略了return self一部分(克林特·米勒的正确答案),那么您将盯着这个废话:

Traceback (most recent call last):
  File "tests/simplestpossible.py", line 17, in <module>                                                                                                                                                          
    fixture.method()                                                                                                                                                                                              
AttributeError: 'NoneType' object has no attribute 'method'

希望它能帮助下一个人。


8

只需用try / except语句包装您的析构函数,如果您的全局变量已被处理,它将不会引发异常。

编辑

尝试这个:

from weakref import proxy

class MyList(list): pass

class Package:
    def __init__(self):
        self.__del__.im_func.files = MyList([1,2,3,4])
        self.files = proxy(self.__del__.im_func.files)

    def __del__(self):
        print self.__del__.im_func.files

它将把文件列表填充到保证在调用时存在的del函数中。弱引用代理是为了防止Python或您自己以某种方式删除self.files变量(如果删除了它,则不会影响原始文件列表)。即使存在更多对该变量的引用,如果不是不是要删除此变量,则可以删除代理封装。


2
问题是,如果成员数据丢失了,对我来说为时已晚。我需要这些数据。请参阅上面的代码:我需要文件名才能知道要删除的文件。我简化了我的代码,但是我还需要清理其他数据(即解释器不知道如何清理)。
威廉姆特尔2009年

4

似乎惯用的方法是提供一个close()方法(或类似方法),并明确地调用它。


20
这是我以前使用的方法,但是我遇到了其他问题。除了其他库抛出的异常外,我还需要Python的帮助来清除出现错误时的混乱情况。具体来说,我需要Python来为我调用析构函数,因为否则,代码将很快变得难以管理,并且我肯定会忘记应该调用.close()的退出点。
威廉姆特尔2009年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.