Как использовать менеджер контекста, чтобы избежать использования __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):
        ???

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


person Alex    schedule 15.03.2013    source источник


Ответы (3)


Я предлагаю использовать класс 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, если вам небезразличны такие вещи).

person picomancer    schedule 15.03.2013
comment
Требуется ли возврат в def open_device ? - person Mr_and_Mrs_D; 06.05.2017

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

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

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

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

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

person BrenBarn    schedule 15.03.2013
comment
Тот же комментарий, что и раньше: я заранее не знаю, что произойдет с устройством, но оно будет открыто и использовано в нескольких других методах, классах и т. д. В этом случае мне все равно следует использовать __del__ вместо этого? - person Alex; 15.03.2013
comment
Я думаю, что __del__() - это ваш единственный открытый для этого открытого поведения. Имейте в виду, что __del__() не вызывается только в том случае, если сборщик мусора находит ссылочный цикл, включающий ваш класс. Вы даже можете обнаружить и устранить это вручную в своем коде, периодически проверяя gc.garbage< /a> и прерывание циклов ссылок, вызывающих проблему. - person Cartroo; 15.03.2013

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

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

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

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

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

person Cartroo    schedule 15.03.2013
comment
Ваше решение тоже работает только в рамках одной функции! Что, если я хочу открыть что-то в методе my_method(), сделать что-то еще в другой функции? Ваше предложение не сработает. - person Alex; 15.03.2013
comment
Как я указал в конце своего ответа, предложение with будет работать только внутри функции, в которой оно используется - именно так оно и определено. Вы можете использовать with в определении класса, но это будет применяться только во время обработки определения. Если вам нужен одинаковый эффект на протяжении всего жизненного цикла класса, вам нужно использовать __del__(), даже если он страдает от проблем. Лучшее решение, вероятно, состоит в том, чтобы открывать и закрывать устройство только в каждой функции. Извините, если я не сделал это достаточно ясно. - person Cartroo; 15.03.2013