Diferenciar entre los posibles orígenes de excepciones planteadas a partir de una withdeclaración compuesta
Diferenciar entre excepciones que ocurren en una withdeclaración es complicado porque pueden originarse en diferentes lugares. Se pueden generar excepciones desde cualquiera de los siguientes lugares (o funciones llamadas allí):
ContextManager.__init__
ContextManager.__enter__
- el cuerpo de la
with
ContextManager.__exit__
Para obtener más detalles, consulte la documentación sobre Tipos de Context Manager .
Si queremos distinguir entre estos diferentes casos, simplemente envolviendo el with en a try .. exceptno es suficiente. Considere el siguiente ejemplo (usando ValueErrorcomo ejemplo pero, por supuesto, podría ser sustituido por cualquier otro tipo de excepción):
try:
with ContextManager():
BLOCK
except ValueError as err:
print(err)
Aquí exceptcapturará excepciones que se originan en los cuatro lugares diferentes y, por lo tanto, no permite distinguir entre ellas. Si movemos la instanciación del objeto del administrador de contexto fuera del with, podemos distinguir entre__init__ y 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
Efectivamente, esto solo ayudó con la __init__parte, pero podemos agregar una variable centinela adicional para verificar si el cuerpo delwith inicio comenzó a ejecutarse (es decir, diferenciar entre __enter__y los demás):
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
La parte difícil es diferenciar entre las excepciones que se originan BLOCKy __exit__porque se withpasará una excepción que escapa del cuerpo de la voluntad a la __exit__cual puede decidir cómo manejarla (ver los documentos ). Sin embargo __exit__, si surge, la excepción original será reemplazada por la nueva. Para tratar estos casos, podemos agregar una exceptcláusula general en el cuerpo del testamento simplemente no se ejecutará).with archivo para almacenar cualquier posible excepción que de otro modo hubiera pasado desapercibida y compararla con la capturada en el exterior exceptmás adelante; si son iguales, esto significa que el origen fue BLOCKo de lo contrario lo fue __exit__(en caso de que __exit__suprima la excepción al devolver un valor verdadero lo más externoexcept
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)
Enfoque alternativo utilizando la forma equivalente mencionada en PEP 343
PEP 343 - La declaración "con" especifica una versión equivalente "no con" de la withdeclaración. Aquí podemos envolver fácilmente las diversas partes try ... excepty así diferenciar entre las diferentes fuentes de error potenciales:
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)
Por lo general, un enfoque más simple funcionará bien
La necesidad de tal manejo de excepciones especiales debe ser bastante rara y normalmente envolver todo withen un try ... exceptbloque será suficiente. Especialmente si las diversas fuentes de error están indicadas por diferentes tipos de excepción (personalizados) (los gestores de contexto deben diseñarse en consecuencia) podemos distinguir fácilmente entre ellos. Por ejemplo:
try:
with ContextManager():
BLOCK
except InitError: # raised from __init__
...
except AcquireResourceError: # raised from __enter__
...
except ValueError: # raised from BLOCK
...
except ReleaseResourceError: # raised from __exit__
...
withdeclaración no rompe mágicamente unatry...exceptdeclaración circundante .