关于析构函数:如何正确清理Python对象?

How do I correctly clean up a Python object?

1
2
3
4
5
6
7
8
9
class Package:
    def __init__(self):
        self.files = []

    # ...

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

上面的__del__(self)失败,出现attributeError异常。我理解python不能保证"全局变量"(此上下文中的成员数据)的存在。当调用__del__()时。如果是这种情况,这就是异常的原因,那么如何确保对象正确销毁?


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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
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)

然后,当有人想使用你的课程时,他们会做以下操作:

1
2
with Package() as package_obj:
    # use package_obj

变量包"obj"将是package类型的实例(它是__enter__方法返回的值)。它的__exit__方法将自动被调用,不管是否发生异常。

您甚至可以进一步采用这种方法。在上面的示例中,仍有人可以使用其构造函数实例化包,而不必使用with子句。你不想这样。您可以通过创建定义__enter____exit__方法的packageresource类来解决这个问题。然后,package类将严格定义在__enter__方法内并返回。这样,调用方就无法在不使用with语句的情况下实例化package类:

1
2
3
4
5
6
7
8
9
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()

您可以按如下方式使用:

1
2
with PackageResource() as package_obj:
    # use package_obj


标准方法是使用atexit.register

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 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的代码演示:

1
2
3
4
5
6
7
8
9
10
11
$ 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...


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

1
2
3
4
5
6
7
@contextlib.contextmanager
def packageResource():
    class Package:
        ...
    package = Package()
    yield package
    package.cleanup()

或者,虽然可能不是Python,但您可以覆盖Package.__new__

1
2
3
4
5
6
7
8
9
10
11
12
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,在这种情况下,您可以通过with contextlib.closing(Package(...))使用未修改的Package类,也可以将其__new__重写为更简单的

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

这个构造函数是继承的,所以您可以简单地继承,例如

1
2
3
class SubPackage(Package):
    def close(self):
        pass


我不认为有可能在调用__del__之前删除成员。我的猜测是,您的特定属性错误的原因是在其他地方(可能您在其他地方错误地删除self.file)。

然而,正如其他人指出的,您应该避免使用__del__。这主要是因为使用__del__的实例不会被垃圾收集(只有当引用计数达到0时才会释放)。因此,如果您的实例涉及循环引用,那么它们将在应用程序运行期间一直存在于内存中。(不过,我可能对这一切都弄错了,我必须再次阅读GC文档,但我确信它是这样工作的)。


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

即使__init__未被正确执行或抛出异常,也将调用__del__

来源


更好的选择是使用weakref.finalize。请参阅Finalizer对象中的示例,并将Finalizer与uu del_uu()方法进行比较。


只需用try/except语句包装析构函数,如果已经释放了全局变量,则不会引发异常。

编辑

试试这个:

1
2
3
4
5
6
7
8
9
10
11
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函数中,该函数保证在调用时存在。weakref代理是为了防止python或您自己以某种方式删除self.files变量(如果删除了该变量,则不会影响原始文件列表)。如果没有这种情况,即使对变量有更多的引用,也要删除它,那么您可以删除代理封装。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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部分,你就会盯着这胡说八道:

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

我为此花了半天时间。希望它能帮助下一个人。


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