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