Розмежування між можливими джерелами винятків, зібраних із складеного 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заяву.