Как использовать диспетчер контекста, чтобы избежать использования __del__ в python?

Как известно, метод 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):
        ???

но как с этим справиться? Может, я забыл что-то важное? Или я могу использовать диспетчер контекста только внутри функции, а не как переменную внутри области класса?

Ответ 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 после этого. Когда управление выходит из блока по любому пути; включая исключение бросания – будет выполняться оставшаяся часть функции open_device.

Я не уверен, что (а) ваш класс-оболочка добавляет функциональность к API нижнего уровня или (б) если он только что-то включается, поэтому вы можете иметь менеджер контекста. Если (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, если вы заботитесь о таких вещах).

Ответ 2

Проблема заключается не в том, что вы используете его в классе, а в том, что вы хотите оставить устройство "открытым" способом: вы его открываете, а затем просто оставляете его открытым. Диспетчер контекста предоставляет возможность открыть некоторый ресурс и использовать его в относительно коротком, ограниченном виде, убедившись, что он закрыт в конце. Ваш существующий код уже небезопасен, потому что, если происходит некоторая авария, вы не можете гарантировать, что ваш __del__ будет вызван, поэтому устройство может быть оставлено открытым.

Не зная точно, что такое устройство и как оно работает, трудно сказать больше, но основная идея заключается в том, что, если возможно, лучше всего открыть устройство только тогда, когда вам нужно его использовать, а затем закрыть его сразу после этого. Итак, ваш processing - это то, что может понадобиться изменить, к чему-то более похожим:

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

Если self.device является соответствующим образом написанным менеджером контекста, он должен открыть устройство в __enter__ и закрыть его в __exit__. Это гарантирует, что устройство будет закрыто в конце блока with.

Конечно, для некоторых видов ресурсов это невозможно сделать (например, поскольку открытие и закрытие устройства теряет важное состояние или является медленной операцией). Если это ваш случай, вы застряли в использовании __del__ и живете с его подводными камнями. Основная проблема заключается в том, что нет надежного способа оставить устройство "открытым", но при этом гарантировать, что он будет закрыт даже в случае необычного сбоя программы.

Ответ 3

Я не совсем уверен, что вы спрашиваете. Экземпляр диспетчера контекста может быть членом класса - вы можете повторно использовать его как можно больше предложений with, и каждый раз будут вызываться методы __enter__() и __exit__().

Итак, как только вы добавили эти методы в MyWrapper, вы можете построить его в MyClass так же, как и выше. И тогда вы сделаете что-то вроде:

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

Это вызовет методы __enter__() и __exit__() для экземпляра, созданного в конструкторе.

Однако предложение with может охватывать только функцию - если вы используете предложение with в конструкторе, то он вызовет __exit__() перед выходом из конструктора. Если вы хотите это сделать, единственный способ - использовать __del__(), у которого есть свои проблемы, как вы уже упоминали. Вы можете открыть и закрыть устройство только тогда, когда вам это нужно, используя with, но я не знаю, соответствует ли это вашим требованиям.