Diferenciar entre los posibles orígenes de excepciones planteadas a partir de una with
declaración compuesta
Diferenciar entre excepciones que ocurren en una with
declaració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 .. except
no es suficiente. Considere el siguiente ejemplo (usando ValueError
como 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í except
capturará 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 BLOCK
y __exit__
porque se with
pasará 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 except
clá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 except
más adelante; si son iguales, esto significa que el origen fue BLOCK
o 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 with
declaración. Aquí podemos envolver fácilmente las diversas partes try ... except
y 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 with
en un try ... except
bloque 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__
...
with
declaración no rompe mágicamente unatry...except
declaración circundante .