如何使用上下文管理器在Python中避免使用__del__?

13

众所周知,Python中的__del__方法不能用于清理重要的东西,因为不能保证该方法一定会被调用。替代方法是使用上下文管理器,如几个线程中所描述的。

但我不太明白如何重写一个类来使用上下文管理器。具体来说,我有一个简单的(不起作用的)示例,其中包装器类打开并关闭设备,并且在任何情况下类实例超出其范围时都将关闭设备(异常等)。

第一个文件mydevice.py是一个标准的包装器类,用于打开和关闭设备:

class MyWrapper(object):
    def __init__(self, device):
        self.device = device

    def open(self):
        self.device.open()

    def close(self):
        self.device.close()

    def __del__(self):
        self.close()

这个类被另一个类 myclass.py 使用:

import mydevice


class MyClass(object):

    def __init__(self, device):

        # calls open in mydevice
        self.mydevice = mydevice.MyWrapper(device)
        self.mydevice.open()

    def processing(self, value):
        if not value:
            self.mydevice.close()
        else:
            something_else()

我的问题是:当我在mydevice.py中实现上下文管理器时,使用__enter____exit__方法,我该如何在myclass.py中处理这个类?我需要像这样做:

def __init__(self, device):
    with mydevice.MyWrapper(device):
        ???

那么该如何处理呢?也许我忽略了什么重要的东西吗?或者我只能在函数内部使用上下文管理器,而不能在类范围内作为变量使用吗?

3个回答

18

我建议使用contextlib.contextmanager类,而不是编写一个实现__enter____exit__的类。以下是它的工作方式:

class MyWrapper(object):
    def __init__(self, device):
        self.device = device

    def open(self):
        self.device.open()

    def close(self):
        self.device.close()

    # I assume your device has a blink command
    def blink(self):
        # do something useful with self.device
        self.device.send_command(CMD_BLINK, 100)

    # there is no __del__ method, as long as you conscientiously use the wrapper

import contextlib

@contextlib.contextmanager
def open_device(device):
    wrapper_object = MyWrapper(device)
    wrapper_object.open()
    try:
        yield wrapper_object
    finally:
        wrapper_object.close()
    return

with open_device(device) as wrapper_object:
     # do something useful with wrapper_object
     wrapper_object.blink()

以@符号开头的行被称为装饰器,它会修改下一行的函数声明。

当遇到with语句时,open_device()函数将执行到yield语句。在这个yield语句中返回的值会赋给可选的as子句的目标变量,这里是wrapper_object。之后你可以像使用普通Python对象一样使用它。当控制流通过任何路径退出块时,包括抛出异常,open_device函数的剩余部分将被执行。

我不确定你的包装类是是否添加了功能到更低级别的API中,还是只包含一个上下文管理器,如果是后者,那么你可以完全不使用它,因为contextlib已经替你处理了。这是代码可能会是这样:

import contextlib

@contextlib.contextmanager
def open_device(device):
    device.open()
    try:
        yield device
    finally:
        device.close()
    return

with open_device(device) as device:
     # do something useful with device
     device.send_command(CMD_BLINK, 100)

99%的上下文管理器用法可以使用contextlib.contextmanager来完成。它是一种非常有用的API类(如果您关心这样的事情,实现方式也是对较低级别Python管道的创造性使用)。


def open_device 函数中需要返回吗? - Mr_and_Mrs_D

4
问题不在于你使用它在一个类中,而是你想以“开放的”方式离开设备:你打开它,然后只是把它留开着。上下文管理器提供了一种在相对短暂、受限的方式下打开某些资源并使用它的方式,确保在结束时关闭它。你现有的代码已经不安全,因为如果发生崩溃,无法保证你的__del__将被调用,因此设备可能会被留在开放状态。
如果不知道设备是什么以及它如何工作,更难说些什么,但基本思想是,如果可能的话,最好仅在需要使用设备时立即打开它,然后立即关闭它。所以你的processing是可能需要改变,变成这样:
def processing(self, value):
     with self.device:
        if value:
            something_else()

如果self.device是一个正确编写的上下文管理器,它应该在__enter__中打开设备,在__exit__中关闭设备。这确保了设备将在with块结束时关闭。
当然,对于某些资源来说,这是不可能的(例如因为打开和关闭设备会丢失重要状态或者是一个缓慢的操作)。如果您的情况是这样的,那么您就只能使用__del__并接受其缺点。基本问题是没有一种绝对可靠的方法可以让设备“开放式”但仍然保证在某些异常程序故障的情况下关闭设备。

我事先不知道设备会发生什么情况,但它将被打开并在其他多个方法、类中使用。在这种情况下,我是否应该仍然使用 __del__ 方法? - Alex
我认为__del __()是您唯一可以实现这种开放式行为的方法。请记住,仅当垃圾收集器发现涉及您的类的引用循环时,才不会调用__del__()。您甚至可以通过定期检查gc.garbage并打破导致问题的引用循环,在代码中手动检测和解决此问题。 - Cartroo

0

我不太确定你在问什么。上下文管理器实例可以是类成员 - 你可以在尽可能多的with子句中重复使用它,每次都会调用__enter__()__exit__()方法。

因此,一旦你将这些方法添加到MyWrapper中,你可以像上面那样在MyClass中构造它。然后你会做一些像这样的事情:

def my_method(self):
    with self.mydevice:
        # Do stuff here

这将调用您在构造函数中创建的实例上的__enter__()__exit__()方法。

然而,with子句只能跨越一个函数 - 如果您在构造函数中使用with子句,则会在退出构造函数之前调用__exit__()。如果您想要这样做,唯一的办法就是使用__del__(),但它也有自己的问题,正如您已经提到的。您可以使用with在需要时打开和关闭设备,但我不知道是否符合您的要求。


你的解决方案仅适用于一个函数内部!如果我想在my_method()方法中打开某些东西,然后在另一个函数中执行其他操作怎么办?那么你的建议就行不通了。 - Alex
正如我在答案末尾指出的那样,with子句仅在使用它的函数内起作用 - 这就是它定义的方式。您可以在类定义中使用with,但它只适用于处理定义的过程。如果您想在类的生命周期内实现相同的效果,则需要使用__del __(),尽管它存在问题。更好的解决方案可能是仅在每个函数中打开和关闭设备。如果我没有表述清楚,我很抱歉。 - Cartroo

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