【问题标题】:How to use the context manager to avoid the use of __del__ in python?如何使用上下文管理器避免在python中使用__del__?
【发布时间】:2013-03-03 20:52:29
【问题描述】:

众所周知,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()

我的问题:当我使用__enter____exit__ 方法在mydevice.py 中实现上下文管理器时,如何在myclass.py 中处理此类?我需要做类似的事情

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

但是如何处理呢?也许我忽略了一些重要的事情?或者我可以只在函数内使用上下文管理器,而不是作为类范围内的变量吗?

【问题讨论】:

    标签: python contextmanager


    【解决方案1】:

    我建议使用 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()
    

    以 at 符号开头的行称为 装饰器。它修改了下一行的函数声明。

    当遇到with 语句时,open_device() 函数将执行到yield 语句。 yield 语句中的值在作为可选as 子句目标的变量中返回,在本例中为wrapper_object。此后,您可以像使用普通 Python 对象一样使用该值。当控制通过 any 路径从块中退出时——包括抛出异常——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 管道的创造性使用)。

    【讨论】:

    • def open_device需要退货吗?
    【解决方案2】:

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

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

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

    如果self.device 是一个适当编写的上下文管理器,它应该在__enter__ 中打开设备并在__exit__ 中关闭它。这确保了设备将在 with 块的末尾关闭。

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

    【讨论】:

    • 与之前的评论相同:我事先不知道设备会发生什么,但它将被打开并用于其他几种方法、类等。这种情况我还应该使用__del__代替?
    • 我认为__del__() 是您对这种开放式行为的唯一开放。请记住,只有当垃圾收集器找到涉及您的类的引用循环时,才会调用 __del__()。您甚至可以通过定期检查 gc.garbage 并打破导致问题的引用循环,在代码中手动检测并解决此问题。
    【解决方案3】:

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

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

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

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

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

    【讨论】:

    • 您的解决方案也只适用于一个功能!如果我想在方法my_method() 中打开一些东西,在不同的函数中做一些其他的事情怎么办?您的建议不起作用。
    • 正如我在回答末尾指出的那样,with 子句仅在使用它的函数内有效——这就是它的定义方式。您可以在类定义中使用with,但它仅在处理定义时适用。如果您希望在课程的整个生命周期中都获得相同的效果,那么您将需要使用__del__(),即使它存在问题。更好的解决方案可能是只在每个功能中打开和关闭设备。抱歉,如果我说得不够清楚。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-12-25
    • 2020-04-09
    • 1970-01-01
    • 2013-05-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多