Python中使用__del__方法关闭类似连接的对象的Pythonic方式

5

我正在编写一个类似于连接的对象,它实现了上下文管理器。强烈建议编写以下内容:

with MyConnection() as con:
    # do stuff

当然,也可以这样做:

con = MyConnection()
# do stuff
con.close()

但是未能关闭连接会带来一些问题。因此,在__del__()中关闭连接似乎是一个不错的主意:

def __del__(self):
    self.close()

这看起来很不错,但有时会导致错误:

Exception ignored in: [...]
Traceback (most recent call last):
  File "...", line xxx, in __del__()
TypeError: 'NoneType' object is not callable

似乎有时在调用__del__()方法时,关闭方法已经被销毁。

因此,我正在寻找一种好的方法来鼓励Python在销毁时正常关闭连接。如果可能,我想避免在close()__del__()中重复编写代码。


可能是重复问题:https://dev59.com/GnRA5IYBdhLWcg3wsgBP - Paul Seeb
如果你正在编写一个上下文管理器,请使用__exit__ - jonrsharpe
感谢您的评论 - 我知道使用上下文管理器是一个不错的解决方案 - 但用户不能被强制使用它。我希望避免系统崩溃,如果用户未能正确关闭连接。 - cel
为避免代码重复,让 close 调用 __del__。不要过于保护用户——毕竟他们是程序员,应该足够聪明地使用你提供给他们的 API——只需确保你有良好的文档,其余的就交给他们了。 - Ethan Furman
在close方法中调用__del__有点丑陋。我认为你是对的 - 太过关注用户误用代码可能不是一个好主意,特别是当代码变得相当混乱时。 - cel
6个回答

12

如果你真的想防止用户没有关闭连接,你可以只在__enter__中初始化它,或者你可以添加一个标记来检测上下文管理器是否已将其初始化。例如,像这样的东西:

class MyConnection(object):

    safely_initialized = False

    def __enter__(self):
        # Init your connection
        self.safely_initialized = True
        return self

    def do_something(self):
        if not self.safely_initialized:
            raise Exception('You must initialize the connection with a context manager!')
        # Do something

    def __exit__(self, type, value, traceback):
        # Close your connection

只有在上下文管理器中使用时,该连接才会被初始化。


所以,与其试图在他们选择错误之后进行清理,不如只有在使用支持的方法时才运行。太好了。 - Ethan Furman
是的,那可能会起作用。但它会强制用户使用with语句。我不确定我是否想这样做。 - cel
我认为这是目前为止最好的解决方案。然而,我不确定它是否比尝试在__del__方法中强制断开连接更好。这将导致一些丑陋的代码重复,但不强制用户使用with语句。对我来说,正确关闭连接并不是非常重要 - 只要不要有太多连接就好。 - cel
正如其他人所提到的,应该避免使用__del__,因为它可能导致不可预测的结果。所以如果你不想强制用户,那么你就必须接受一些不完全关闭的连接(或查看@ethan-furman的答案)。 - Seb D.
是的,使用__del__方法进行连接清理可能不是一个好主意。我会等到明天 - 如果没有更多的想法,我会将您的答案标记为正确答案。 - cel

3

无法保证__del__方法何时运行。由于您正在使用with语句,请改用__exit__方法。不管语句如何完成(正常完成、异常等),__exit__将在with语句完成后立即调用。


是的,我的当前实现使用上下文管理器,并鼓励用户使用它。但是,如果用户未能正确使用上下文管理器,我正在寻找一种清理方法。 - cel

1

你可以尝试在__del__中调用close方法,并忽略任何异常:

del __del__(self):
    try:
        self.close()
    except TypeError:
        pass

1
是的,我考虑过这个问题,但我认为这并没有解决关键问题。对我来说,在del中调用close方法似乎是一个不好的想法,因为不能保证该方法仍然存在。 - cel
@cel:如果解释器关闭,您对任何事情都没有保证。 - Ethan Furman

1

确实,您不能强迫用户使用良好的编程技巧,如果他们拒绝这样做,您不能为他们负责。

__del__ 的调用时间无法保证 -- 在某些 Python 版本中,它是立即发生的,而在其他版本中,可能要等到解释器关闭时才会发生。因此,虽然不是一个很好的选择,但使用 atexit 可能是可行的 -- 只需确保您注册的函数足够聪明,可以检查资源是否已经关闭/销毁。


是的,我确实不负责糟糕的编程 - 但如果可能的话,避免崩溃和问题还是很好的。虽然我不能确定所有连接都被关闭 - 但其中大部分可能会被关闭。其余的将在解释器关闭期间被强制关闭。 - cel

1

weakref.finalize()允许在对象被垃圾回收或程序退出时执行操作。它从Python 3.4开始提供。

创建对象时,您可以调用finalize(),并为其提供一个清理对象持有的资源的回调函数。

Python文档提供了几个使用示例

>>> import weakref
>>> class Object:
...     pass
...
>>> kenny = Object()
>>> weakref.finalize(kenny, print, "You killed Kenny!")  
<finalize object at ...; for 'Object' at ...>
>>> del kenny
You killed Kenny!

下面是一个代表临时目录的类的示例,当以下情况之一发生时,其内容将被删除:

  • 调用其remove()方法
  • 被垃圾回收
  • 程序退出

以先发生者为准。

class TempDir:
    def __init__(self):
        self.name = tempfile.mkdtemp()
        self._finalizer = weakref.finalize(self, shutil.rmtree, self.name)

    def remove(self):
        self._finalizer()

    @property
    def removed(self):
        return not self._finalizer.alive

0

您可以定义一个close()方法,并在__exit____del__方法中都在is_open条件下调用它,如下所示:

class MyContext:
    def __init__(self, *args):
        self.is_open = False
        self.args = args
        self.open(*self.args)
    def __enter__(self):
        return self
    def __del__(self):
        self.close()
    def __exit__(self, *args):
        self.close()
    def open(self, *args):
        if not self.is_open:
            self.is_open = True
            print("opening: ", args)
        return self
    def close(self):
        if self.is_open:
            self.is_open = False
            print("closing: ", self.args)

这是一个没有上下文管理器的使用示例:

def init_way():
    c = MyContext("path", "openparam")

init_way()

可能的输出:

opening:  ('path', 'openparam')
closing:  ('path', 'openparam')

另一个例子:这次使用相同的类作为上下文管理器:
def context_way():
    with MyContext("path", "openparam") as c:
        print("in with ...")

context_way()

可能的输出:

opening:  ('path', 'openparam')
in with ...
closing:  ('path', 'openparam')

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