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

时间:2013-03-15 08:01:39

标签: python contextmanager

众所周知,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 :(得分:14)

我建议使用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管道的创造性使用)。

答案 1 :(得分:4)

问题不是你在课堂上使用它,而是你想要以“开放式”的方式离开设备:你打开它然后让它打开。上下文管理器提供了一种打开某些资源并以相对较短的包含方式使用它的方法,确保它在最后关闭。您现有的代码已经不安全了,因为如果发生某些崩溃,您无法保证将调用__del__,因此设备可能会保持打开状态。

如果不确切知道设备是什么以及它是如何工作的,很难说更多,但基本的想法是,如果可能的话,最好只在需要使用它时打开设备,然后关闭它之后立即。所以你的processing可能需要改变,更像是:

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

如果self.device是经过适当编写的上下文管理员,则应在__enter__中打开该设备并在__exit__中将其关闭。这可确保设备在with块结束时关闭。

当然,对于某些类型的资源,不可能这样做(例如,因为打开和关闭设备失去重要状态,或者操作缓慢)。如果这是你的情况,你就会被__del__所困扰并陷入困境。基本问题是没有万无一失的方法让设备“开放式”,但仍然保证即使在一些不寻常的程序失败的情况下也会关闭它。

答案 2 :(得分:0)

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

因此,一旦您将这些方法添加到MyWrapper,就可以像{}}一样在MyClass中构建它。然后你会做类似的事情:

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

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

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