如何正确清理Python对象?

wil*_*ell 436 python destructor

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

    # ...

    def __del__(self):
        for file in self.files:
            os.unlink(file)
Run Code Online (Sandbox Code Playgroud)

__del__(self)上面因AttributeError异常而失败.我理解Python__del__()调用时不保证存在"全局变量"(在此上下文中的成员数据?).如果是这种情况并且这是异常的原因,我该如何确保对象正确破坏?

Cli*_*ler 576

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

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

  def __enter__(self)
  def __exit__(self, exc_type, exc_value, traceback)
Run Code Online (Sandbox Code Playgroud)

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

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)
Run Code Online (Sandbox Code Playgroud)

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

with Package() as package_obj:
    # use package_obj
Run Code Online (Sandbox Code Playgroud)

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

您甚至可以将此方法更进一步.在上面的示例中,有人仍然可以使用其构造函数实例化Package而不使用该with子句.你不希望这种情况发生.您可以通过创建定义__enter____exit__方法的PackageResource类来解决此问题.然后,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()
Run Code Online (Sandbox Code Playgroud)

您将使用如下:

with PackageResource() as package_obj:
    # use package_obj
Run Code Online (Sandbox Code Playgroud)

  • 从技术上讲,可以调用PackageResource().__显式输入__(),从而创建一个永远不会最终确定的包......但他们真的必须试图破解代码.可能不用担心. (33认同)
  • @ snooze92你可以给Resource一个\ _ _ _ init _ _\_ _方法,它在自己中存储*args和**kwargs,然后将它们传递给enter方法中的内部类.使用with语句时,在\ _\_ _之前调用\ _ _ _ init _ _\_ _ _ _ _ (4认同)
  • 顺便说一下,如果您使用的是Python 2.5,则需要从__future__ import with_statement开始,以便能够使用with语句. (3认同)
  • 我找到了一篇文章,它有助于说明为什么__del __()按照它的方式行事,并且可以使用上下文管理器解决方案:http://www.andy-pearce.com/blog/posts/2013/Apr/python-destructor -缺点/ (2认同)
  • 如果要传递参数,如何使用那个漂亮而干净的构造?我希望能够使用Resource(param1,param2)作为r:#...` (2认同)

ost*_*ach 39

标准方法是使用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)
Run Code Online (Sandbox Code Playgroud)

但是你应该记住,这将持续所有创建的实例,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...
Run Code Online (Sandbox Code Playgroud)

  • atexit.register 方法的好处是你不必担心类的用户做什么(他们是否使用了 `with`?他们是否明确调用了 `__enter__`?)缺点当然是如果你需要在 python 退出之前进行清理,它不起作用。就我而言,我不在乎是对象超出范围还是直到 python 退出。:) (2认同)

Tob*_*ler 29

作为Clint答案的附录,您可以简化PackageResource使用contextlib.contextmanager:

@contextlib.contextmanager
def packageResource():
    class Package:
        ...
    package = Package()
    yield package
    package.cleanup()
Run Code Online (Sandbox Code Playgroud)

或者,虽然可能不像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):
        ...
Run Code Online (Sandbox Code Playgroud)

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

为了缩短范围,请命名清理函数close并使用contextlib.closing,在这种情况下,您可以使用未修改的Packagewith 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)
Run Code Online (Sandbox Code Playgroud)

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

class SubPackage(Package):
    def close(self):
        pass
Run Code Online (Sandbox Code Playgroud)


Vir*_*ras 16

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

但是,正如其他人指出的那样,你应该避免使用__del__.这样做的主要原因是__del__不会对垃圾实例进行垃圾收集(只有当它们的refcount达到0时才会释放它们).因此,如果您的实例涉及循环引用,则只要应用程序运行,它们就会存在内存中.(虽然我可能会误解所有这些,但我必须再次阅读gc文档,但我相信它会像这样工作).

  • 具有`__del__`的对象如果来自其他具有`__del__`的对象的引用计数为零且无法访问,则可以进行垃圾回收.这意味着如果在具有`__del__`的对象之间有一个引用循环,则不会收集这些引用循环.但是,任何其他情况都应按预期解决. (4认同)
  • “从 Python 3.4 开始,__del__() 方法不再阻止引用循环被垃圾收集,并且在解释器关闭期间模块全局变量不再被强制为 None。因此,此代码在 CPython 上应该可以正常运行。” - https://docs.python.org/3.6/library/weakref.html#comparing-finalizers-with-del-methods (3认同)

Chr*_*ris 14

一个好主意是将这两种方法结合起来。

实现一个上下文管理器来进行显式的生命周期处理,并处理自动清理,以防用户忘记或不方便这样做,就像在解释器中一样。保证销毁时的清理最好由 完成weakref.finalize

许多图书馆实际上就是这样做的。如果不使用上下文管理器,有些会发出警告,以鼓励人们使用它。

一旦设置好,一个好处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()
Run Code Online (Sandbox Code Playgroud)

weakref.finalize返回一个可调用的终结器对象,当 obj 被垃圾回收时将调用该对象。与普通的弱引用不同,终结器将始终存在,直到引用对象被收集为止。

一个缺点是,必须在静态方法内实现清理逻辑,因为此时对象不再存在。

这样做的一个好处atexit.register是,在解释器关闭之前,对象不会保留在内存中。当对象被垃圾回收时,清理就会发生。

与 不同的是object.__del__weakref.finalize保证在解释器关闭时被调用,因为解释器会对它们进行特殊跟踪。


小智 11

我认为__init__如果有更多的代码而不是显示的问题可能会出现问题?

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

资源

  • 听起来很可能.使用`__del__`时避免这个问题的最好方法是在类级别显式声明所有成员,确保它们始终存在,即使`__init__`失败.在给定的例子中,`files =()`可以工作,但大多数情况下你只需要指定`None`; 在任何一种情况下,你仍然需要在`__init__`中分配实际值. (2认同)

SCG*_*CGH 10

更好的选择是使用weakref.finalize.请参阅Finalizer对象中的示例和使用__del __()方法比较终结器.

  • IMO,这真的是最好的答案。它结合了垃圾收集时清理的可能性和出口清理的可能性。需要注意的是,python 2.7 没有weakref.finalize。 (3认同)

Unk*_*own 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
Run Code Online (Sandbox Code Playgroud)

它会将文件列表填入保证在调用时存在的del函数中.weakref代理是为了防止Python,或者你自己以某种方式删除self.files变量(如果它被删除,那么它不会影响原始文件列表).如果不是这种情况被删除,即使有更多的变量引用,那么您可以删除代理封装.

  • 问题是,如果会员数据消失,对我来说为时已晚.我需要那些数据.请参阅上面的代码:我需要文件名来了解要删除的文件.我简化了我的代码,我需要清理自己的其他数据(即解释器不知道如何清理). (2认同)

use*_*284 7

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

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()
Run Code Online (Sandbox Code Playgroud)

重要提示:自我回报


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

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

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


Bas*_*ard 5

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

  • 这是我以前使用的方法,但我遇到了其他问题。由于其他库到处抛出异常,我需要 Python 的帮助来在出现错误的情况下清理混乱。具体来说,我需要 Python 为我调用析构函数,否则代码会很快变得难以管理,我肯定会忘记调用 .close() 的退出点。 (22认同)