Diferenciando entre as possíveis origens de exceções levantadas de uma with
declaração composta
A diferenciação entre exceções que ocorrem em uma with
instrução é complicada porque elas podem se originar em lugares diferentes. Exceções podem ser geradas a partir de um dos seguintes locais (ou funções aqui denominadas):
ContextManager.__init__
ContextManager.__enter__
- o corpo do
with
ContextManager.__exit__
Para mais detalhes, consulte a documentação sobre tipos de gerenciador de contexto .
Se quisermos distinguir entre esses casos diferentes, apenas agrupar o with
em a try .. except
não é suficiente. Considere o seguinte exemplo (usando ValueError
como exemplo, mas é claro que poderia ser substituído por qualquer outro tipo de exceção):
try:
with ContextManager():
BLOCK
except ValueError as err:
print(err)
Aqui, except
serão capturadas exceções originadas em todos os quatro locais diferentes e, portanto, não permitem distinguir entre eles. Se movermos a instanciação do objeto do gerenciador de contexto para fora do with
, poderemos distinguir entre __init__
e 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
Efetivamente, isso apenas ajudou na __init__
parte, mas podemos adicionar uma variável sentinela extra para verificar se o corpo do with
começou a executar (ou seja, diferenciar entre __enter__
os outros):
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
A parte complicada é diferenciar as exceções originadas BLOCK
e __exit__
porque uma exceção que escapa ao corpo do with
será transmitida para __exit__
que possa decidir como lidar com isso (consulte os documentos ). Se, no entanto __exit__
, se elevar, a exceção original será substituída pela nova. Para lidar com esses casos, podemos adicionar uma except
cláusula geral no corpo do with
arquivo para armazenar qualquer exceção em potencial que, de outra forma, passaria despercebida e compará-la com a capturada mais except
tarde no exterior - se forem iguais, significa que a origem foi BLOCK
ou de outra forma era __exit__
(no caso, __exit__
suprime a exceção retornando um valor verdadeiro o mais externoexcept
simplesmente não será executado).
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)
Abordagem alternativa usando o formulário equivalente mencionado no PEP 343
PEP 343 - A declaração "with" especifica uma versão equivalente "non-with" da with
declaração. Aqui, podemos prontamente agrupar as várias partes try ... except
e, assim, diferenciar as diferentes fontes potenciais de erro:
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)
Geralmente, uma abordagem mais simples funciona muito bem
A necessidade desse tratamento de exceção especial deve ser bastante rara e normalmente envolver o todo with
em um try ... except
bloco será suficiente. Especialmente se as várias fontes de erro são indicadas por diferentes tipos de exceção (personalizados) (os gerenciadores de contexto precisam ser projetados de acordo), podemos distinguir facilmente entre elas. Por exemplo:
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
declaração não magicamente quebra umatry...except
declaração circundante .