Python как безопасно обрабатывать исключение внутри менеджера контекста

Я думаю, что я прочитал, что исключения внутри with не позволяют правильному вызову __exit__. Если я ошибаюсь в этой заметке, прошу простить мое невежество.

Итак, у меня есть некоторый псевдокод, моя цель - использовать контекст блокировки, который при __enter__ записывает начало datetime и возвращает идентификатор блокировки, а после __exit__ записывает конец datetime и освобождает блокировку:

def main():
    raise Exception

with cron.lock() as lockid:
    print('Got lock: %i' % lockid)
    main()

Как я могу все еще повышать ошибки в дополнение к существующему контексту?

Примечание. Я намеренно поднимаю базовое исключение в этом псевдокоде, поскольку я хочу выйти безопасно при любом исключении, а не только ожидаемые исключения.

Примечание. Альтернативные/стандартные методы предотвращения concurrency не имеют значения, я хочу применить эти знания к любому управлению общим контекстом. Я не знаю, имеют ли разные контексты разные причуды.

PS. Соответствует ли блок finally?

Ответ 1

Метод __exit__ называется нормальным, если контекстный менеджер разбит на исключение. Фактически, параметры, переданные в __exit__, имеют отношение к обработке этого случая! Из документы:

object.__exit__(self, exc_type, exc_value, traceback)

Выход из контекста среды выполнения, связанного с этим объектом. Параметры описывают исключение, вызвавшее выход из контекста. Если контекст был исключен без исключения, все три аргумента будут None.

Если предоставляется исключение, и метод хочет подавить исключение (т.е. предотвратить его распространение), он должен вернуть истинное значение. В противном случае исключение будет обработано обычно после выхода из этого метода.

Обратите внимание, что методы __exit__() не должны повторно обрабатывать исключение прошедшего; это ответственная сторона.

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

DummyContextManager(object):
    def __enter__(self):
        print('Entering...')
    def __exit__(self, exc_type, exc_value, traceback):
        print('Exiting...')  
        # If we returned True here, any exception would be suppressed!

with DummyContextManager() as foo:
    raise Exception()

Когда вы запускаете этот код, вы должны видеть все, что хотите (может быть, не в порядке, поскольку print имеет тенденцию попадать в середину трассировки):

Entering...
Exiting...
Traceback (most recent call last):
  File "C:\foo.py", line 8, in <module>
    raise Exception()
Exception

Ответ 2

Лучшая практика при использовании @contextlib.contextmanager была не совсем понятна для меня из приведенного выше ответа. Я перешел по ссылке link в комментарии @BenUsman.

Если вы пишете менеджер контекста, вы должны заключить блок yield в блок try-finally:

from contextlib import contextmanager

@contextmanager
def managed_resource(*args, **kwds):
    # Code to acquire resource, e.g.:
    resource = acquire_resource(*args, **kwds)
    try:
        yield resource
    finally:
        # Code to release resource, e.g.:
        release_resource(resource)

>>> with managed_resource(timeout=3600) as resource:
...     # Resource is released at the end of this block,
...     # even if code in the block raises an exception