Розмежування між можливими джерелами винятків, зібраних із складеного with
твердження
Розрізняти винятки, які трапляються у with
висловлюванні, є складним, оскільки вони можуть виникнути в різних місцях. Винятки можна збільшити з будь-якого з наступних місць (або функцій, що називаються в ньому):
ContextManager.__init__
ContextManager.__enter__
- тіло
with
ContextManager.__exit__
Докладніше див. Документацію про типи диспетчерів контекстів .
Якщо ми хочемо розрізнити між цими різними випадками, достатньо лише with
перетворити їх у форму 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
твердження "не з" . Тут ми можемо охопити різні частини 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
заяву.