如何正确地清理 Python 对象?

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


# ...


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

上面的 __del__(self)失败,出现 AttributeError 异常。我理解 巨蟒不能保 C存在“全局变量”(在这种情况下成员数据?)当调用 __del__()时。如果是这种情况,这是异常的原因,我如何确保对象正确销毁?

531687 次浏览

只要用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

它将把文件列表填充到函数中,该函数保证在调用时存在。weakref代理是为了防止Python或您自己删除self。Files变量(如果它被删除,那么它不会影响原始文件列表)。如果即使有更多对变量的引用,也没有删除该变量,那么可以删除代理封装。

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

我建议使用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子句。你不会希望发生这种事的。您可以通过创建一个PackageResource类来修复这个问题,这个类定义了__enter____exit__方法。然后,Package类将在__enter__方法中严格定义并返回。这样,调用者不使用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

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

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

我认为问题可能是在__init__,如果有更多的代码显示?

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

Source .

作为克林特的回答的附录,你可以使用contextlib.contextmanager来简化PackageResource:

@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,在这种情况下,您可以通过with contextlib.closing(Package(...))使用未修改的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

标准的方法是使用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...

更好的选择是使用weakref.finalize。参见终结器对象比较终结器和__del__()方法的例子。

下面是一个最小的工作框架:

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()

重要:# EYZ1


如果你像我一样,忽略了return self部分(克林特·米勒的正确答案),你将会看到这些无意义的东西:

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

希望它能帮助到下一个人。

atexit.register是已经在ostrakach的回答中提到的标准方式。

但是,必须注意的是,不能依赖于对象可能被删除的顺序,如下例所示。

import atexit


class A(object):


def __init__(self, val):
self.val = val
atexit.register(self.hello)


def hello(self):
print(self.val)




def hello2():
a = A(10)


hello2()
a = A(20)

在这里,顺序似乎是合法的,因为它与创建对象的顺序相反,程序给出的输出如下:

20
10

然而,在一个较大的程序中,当python的垃圾收集启动超出其生命周期的对象时,将首先被销毁。

绝对最好的方法是将两种方法结合起来。

实现用于显式生命周期处理的上下文管理器。以及句柄清理,以防用户忘记它或不方便使用with语句。这最好由weakref.finalize完成。

许多库实际上是这样做的。根据严重程度,你可以发出警告。

import os
from typing import List
import weakref


class Package:
def __init__(self):
self.files = []
self._finalizer = weakref.finalize(self, self._cleanup_files, self.files)


@staticmethod
def _cleanup_files(files: List):
for file in files:
os.unlink(file)


def __enter__(self):
return self


def __exit__(self, exc_type, exc_value, traceback):
self._finalizer()

weakref.finalize返回一个可调用的终结器对象,该对象将在obj被垃圾收集时被调用。与普通的弱引用不同,终结器将始终存在,直到引用对象被收集,极大地简化了生命周期管理。

atexit.register不同的是,在解释器关闭之前,对象不会保存在内存中。

object.__del__不同,weakref.finalize保证在解释器关闭时被调用。所以更安全。