Вызов __enter__ и __exit__ вручную
Я googled calling __enter__ manually
, но не повезло. Поэтому представьте себе, что у меня есть класс соединителей MySQL, который использует функции __enter__
и __exit__
(первоначально используемые с оператором with
) для подключения/отключения от базы данных.
И пусть у вас есть класс, который использует 2 из этих соединений (например, для синхронизации данных). Примечание: это не мой реальный сценарий, но это, пожалуй, самый простой пример.
Самый простой способ заставить все работать вместе - это класс:
class DataSync(object):
def __init__(self):
self.master_connection = MySQLConnection(param_set_1)
self.slave_connection = MySQLConnection(param_set_2)
def __enter__(self):
self.master_connection.__enter__()
self.slave_connection.__enter__()
return self
def __exit__(self, exc_type, exc, traceback):
self.master_connection.__exit__(exc_type, exc, traceback)
self.slave_connection.__exit__(exc_type, exc, traceback)
# Some real operation functions
# Simple usage example
with DataSync() as sync:
records = sync.master_connection.fetch_records()
sync.slave_connection.push_records(records)
Q: нормально ли (что-то не так), чтобы вызвать __enter__
/__exit__
вручную, как это?
Pylint 1.1.0 не выдавал никаких предупреждений об этом, и я не нашел статьи об этом (ссылка google в начале).
А как насчет вызова:
try:
# Db query
except MySQL.ServerDisconnectedException:
self.master_connection.__exit__(None, None, None)
self.master_connection.__enter__()
# Retry
Это хорошая/плохая практика? Почему?
Ответы
Ответ 1
Нет, в этом нет ничего плохого. Есть даже места в стандартной библиотеке, которые это делают. Как multiprocessing
модуль:
class SemLock(object):
def __init__(self, kind, value, maxvalue, *, ctx):
...
try:
sl = self._semlock = _multiprocessing.SemLock(
kind, value, maxvalue, self._make_name(),
unlink_now)
except FileExistsError:
pass
...
def __enter__(self):
return self._semlock.__enter__()
def __exit__(self, *args):
return self._semlock.__exit__(*args)
Или tempfile
модуль:
class _TemporaryFileWrapper:
def __init__(self, file, name, delete=True):
self.file = file
self.name = name
self.delete = delete
self._closer = _TemporaryFileCloser(file, name, delete)
...
# The underlying __enter__ method returns the wrong object
# (self.file) so override it to return the wrapper
def __enter__(self):
self.file.__enter__()
return self
# Need to trap __exit__ as well to ensure the file gets
# deleted when used in a with statement
def __exit__(self, exc, value, tb):
result = self.file.__exit__(exc, value, tb)
self.close()
return result
Стандартные примеры библиотек не вызывают __enter__
/__exit__
для двух объектов, но если у вас есть объект, ответственный за создание/уничтожение контекста для нескольких объектов, а не только один, вызов __enter__
/__exit__
для всех из них в порядке.
Единственный потенциальный доступ - правильно обрабатывать возвращаемые значения вызовов __enter__
__exit__
для объектов, которыми вы управляете. С помощью __enter__
вам нужно убедиться, что вы возвращаете все, что state
требуется для пользователя вашего объекта-обертки, чтобы вернуться из вызова with ... as <state>:
. С помощью __exit__
вам нужно решить, хотите ли вы распространять любое исключение, возникшее внутри контекста (путем возврата False
), или подавить его (путем возврата True
). Ваши управляемые объекты могут попытаться сделать это в любом случае, вам нужно решить, что имеет смысл для объекта-оболочки.
Ответ 2
Ваш первый пример - не очень хорошая идея:
-
Что произойдет, если slave_connection.__enter__
выдает исключение:
-
master_connection
приобретает ресурс
-
slave_connection
не работает
-
DataSync.__enter__
запрещает исключение
-
DataSync.__exit__
не запускается
-
master_connection
никогда не очищается!
- Потенциал для плохих вещей
-
Что произойдет, если master_connection.__exit__
выдает исключение?
-
DataSync.__exit__
закончил рано
-
slave_connection
никогда не очищается!
- Потенциал для плохих вещей
contextlib.ExitStack
может помочь здесь:
def __enter__(self):
with ExitStack() as stack:
stack.enter_context(self.master_connection)
stack.enter_context(self.slave_connection)
self._stack = stack.pop_all()
return self
def __exit__(self, exc_type, exc, traceback):
self._stack.__exit__(self, exc_type, exc, traceback)
Задавая те же вопросы:
-
Что произойдет, если slave_connection.__enter__
выдает исключение:
- Выход с блоком завершен, а
stack
очищает master_connection
- Все в порядке!
-
Что произойдет, если master_connection.__exit__
выдает исключение?
- Неважно,
slave_connection
очищается до того, как это называется
- Все в порядке!
-
Хорошо, что произойдет, если slave_connection.__exit__
выдает исключение?
-
ExitStack
обязательно вызовет master_connection.__exit__
все, что происходит с подчиненным подключением
- Все в порядке!
Нет ничего плохого в вызове __enter__
напрямую, но если вам нужно называть его более чем одним объектом, убедитесь, что вы правильно очистились!