Различение возможных источников исключений, возникающих из составного with
оператора
Различать исключения, возникающие в with
выражении, довольно сложно, поскольку они могут возникать в разных местах. Исключения могут быть вызваны из любого из следующих мест (или вызываемых там функций):
ContextManager.__init__
ContextManager.__enter__
- тело
with
ContextManager.__exit__
Для получения более подробной информации см. Документацию о типах Context Manager .
Если мы хотим провести различие между этими различными случаями, просто завернуть with
в a try .. except
недостаточно. Рассмотрим следующий пример (используя ValueError
в качестве примера, но, конечно, его можно заменить любым другим типом исключения):
try:
with ContextManager():
BLOCK
except ValueError as err:
print(err)
Здесь except
будут вылавливаться исключения, возникающие во всех четырех разных местах и, следовательно, не позволяющие различать их. Если мы переместим создание экземпляра объекта менеджера контекста за пределы with
, мы можем различить __init__
и BLOCK / __enter__ / __exit__
:
try:
mgr = ContextManager()
except ValueError as err:
print('__init__ raised:', err)
else:
try:
with mgr:
try:
BLOCK
except TypeError: # catching another type (which we want to handle here)
pass
except ValueError as err:
# At this point we still cannot distinguish between exceptions raised from
# __enter__, BLOCK, __exit__ (also BLOCK since we didn't catch ValueError in the body)
pass
Фактически это только помогло с __init__
частью, но мы можем добавить дополнительную переменную часового, чтобы проверить, является ли тело with
запущенного для выполнения (то есть, различие между __enter__
и другими):
try:
mgr = ContextManager() # __init__ could raise
except ValueError as err:
print('__init__ raised:', err)
else:
try:
entered_body = False
with mgr:
entered_body = True # __enter__ did not raise at this point
try:
BLOCK
except TypeError: # catching another type (which we want to handle here)
pass
except ValueError as err:
if not entered_body:
print('__enter__ raised:', err)
else:
# At this point we know the exception came either from BLOCK or from __exit__
pass
Сложность состоит в том, чтобы различать исключения, происходящие из BLOCK
и __exit__
потому, что with
будет передано исключение, выходящее за пределы тела, __exit__
которое может решить, как его обработать (см. Документы ). Однако, если он __exit__
поднимется сам, исходное исключение будет заменено новым. Чтобы справиться с этими случаями, мы можем добавить общее except
предложение в теле with
для хранения любого потенциального исключения, которое в противном случае могло бы остаться незамеченным, и сравнить его с тем, которое попало в крайний случай except
позже - если они совпадают, это означает, что источник был BLOCK
или иначе это было __exit__
(в случае __exit__
подавления исключения, возвращая истинное значение самое внешнееexcept
просто не будет казнен).
try:
mgr = ContextManager() # __init__ could raise
except ValueError as err:
print('__init__ raised:', err)
else:
entered_body = exc_escaped_from_body = False
try:
with mgr:
entered_body = True # __enter__ did not raise at this point
try:
BLOCK
except TypeError: # catching another type (which we want to handle here)
pass
except Exception as err: # this exception would normally escape without notice
# we store this exception to check in the outer `except` clause
# whether it is the same (otherwise it comes from __exit__)
exc_escaped_from_body = err
raise # re-raise since we didn't intend to handle it, just needed to store it
except ValueError as err:
if not entered_body:
print('__enter__ raised:', err)
elif err is exc_escaped_from_body:
print('BLOCK raised:', err)
else:
print('__exit__ raised:', err)
Альтернативный подход с использованием эквивалентной формы, упомянутой в PEP 343
PEP 343. - Оператор «with» определяет эквивалентную «не-» версию with
оператора. Здесь мы можем легко обернуть различные части try ... except
и, таким образом, провести различие между различными потенциальными источниками ошибок:
import sys
try:
mgr = ContextManager()
except ValueError as err:
print('__init__ raised:', err)
else:
try:
value = type(mgr).__enter__(mgr)
except ValueError as err:
print('__enter__ raised:', err)
else:
exit = type(mgr).__exit__
exc = True
try:
try:
BLOCK
except TypeError:
pass
except:
exc = False
try:
exit_val = exit(mgr, *sys.exc_info())
except ValueError as err:
print('__exit__ raised:', err)
else:
if not exit_val:
raise
except ValueError as err:
print('BLOCK raised:', err)
finally:
if exc:
try:
exit(mgr, None, None, None)
except ValueError as err:
print('__exit__ raised:', err)
Обычно более простой подход подойдет
Необходимость такой специальной обработки исключений должна быть довольно редкой, и обычно достаточно будет обернуть все with
в try ... except
блок. Особенно, если различные источники ошибок указаны разными (пользовательскими) типами исключений (менеджеры контекста должны быть разработаны соответствующим образом), мы можем легко различить их. Например:
try:
with ContextManager():
BLOCK
except InitError: # raised from __init__
...
except AcquireResourceError: # raised from __enter__
...
except ValueError: # raised from BLOCK
...
except ReleaseResourceError: # raised from __exit__
...
with
Заявление не волшебно разорвать ограждающуюtry...except
заявление.