Различение возможных источников исключений, возникающих из составного 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заявление.