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