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

13 投票
3 回答
8867 浏览
提问于 2025-04-17 19:11

大家都知道,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 个回答

0

我不太明白你在问什么。上下文管理器的实例可以是一个类的成员——你可以在任意多个 with 语句中重复使用它,每次都会调用 __enter__()__exit__() 这两个方法。

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

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

这会在你在构造函数中创建的实例上调用 __enter__()__exit__() 这两个方法。

不过,with 语句只能在一个函数中使用——如果你在构造函数中使用 with 语句,那么在退出构造函数之前,它会先调用 __exit__()。如果你想这样做,唯一的方法就是使用 __del__(),但这也有你之前提到的各种问题。你可以在需要的时候使用 with 来打开和关闭设备,但我不知道这是否符合你的要求。

4

问题不在于你把它放在一个类里,而在于你想以一种“开放”的方式使用设备:你打开它后就一直保持打开状态。上下文管理器提供了一种打开某个资源并在相对较短的时间内使用它的方法,确保在最后会关闭它。你现在的代码已经不安全了,因为如果发生崩溃,你无法保证你的 __del__ 会被调用,这样设备可能会一直保持打开状态。

在不知道设备具体是什么以及它是如何工作的情况下,很难说得更多,但基本的想法是,如果可能的话,最好是在你需要使用设备的时候才打开它,然后立刻关闭。所以你的 processing 可能需要改成更像下面这样的形式:

def processing(self, value):
     with self.device:
        if value:
            something_else()

如果 self.device 是一个写得合适的上下文管理器,它应该在 __enter__ 中打开设备,并在 __exit__ 中关闭设备。这确保了在 with 块结束时,设备会被关闭。

当然,对于某些类型的资源,这样做是不可能的(例如,因为打开和关闭设备会丢失重要的状态,或者是一个比较慢的操作)。如果是这种情况,你就只能使用 __del__,并接受它的一些缺陷。基本的问题是,没有一种万无一失的方法可以让设备保持“开放”状态,同时确保即使在一些异常的程序故障情况下也能关闭它。

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函数剩下的部分会继续执行。

我不太确定(a)你的包装类是否在为一个底层API增加功能,还是(b)你只是想要一个上下文管理器。如果是(b),那么你可能完全可以省略它,因为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机制的创造性使用,如果你对此感兴趣的话)。

撰写回答