Como levantar novamente uma exceção em blocos try / except aninhados?

106

Eu sei que se eu quiser relançar uma exceção, eu simplesmente uso raisesem argumentos no respectivo exceptbloco. Mas dada uma expressão aninhada como

try:
    something()
except SomeError as e:
    try:
        plan_B()
    except AlsoFailsError:
        raise e  # I'd like to raise the SomeError as if plan_B()
                 # didn't raise the AlsoFailsError

como posso aumentar novamente o SomeErrorsem quebrar o rastreamento de pilha? raisesozinho, neste caso, ressuscitaria o mais recente AlsoFailsError. Ou como eu poderia refatorar meu código para evitar esse problema?

Tobias Kienzler
fonte
2
Você já tentou colocar plan_Boutra função que retorna Trueem caso de sucesso e Falseem exceção? Então, o exceptbloco externo poderia ser apenasif not try_plan_B(): raise
Drew McGowen
@DrewMcGowen Infelizmente, o caso mais realista é que isso está dentro de uma função que aceita objetos arbitrários arge eu tentaria chamar o arg.plan_B()que pode gerar um AttributeErrordevido a argnão fornecer um plano B
Tobias Kienzler
Dê uma olhada no módulo traceback: docs.python.org/2/library/traceback.html#traceback-examples
Paco
@Paco Obrigado, eu irei (embora uma resposta já mostre uma maneira mais simples)
Tobias Kienzler
@DrewMcGowen Escrevi uma resposta com base em seu comentário , que parece menos pythônico do que a resposta do usuário4815162342 . Mas isso é devido ao meu desejo de também ter um valor de retorno e permitir plan_Blevantar exceções
Tobias Kienzler

Respostas:

127

A partir do Python 3, o traceback é armazenado na exceção, portanto, um simples raise efará a (principalmente) coisa certa:

try:
    something()
except SomeError as e:
    try:
        plan_B()
    except AlsoFailsError:
        raise e  # or raise e from None - see below

O traceback produzido incluirá um aviso adicional que SomeErrorocorreu durante o manuseio AlsoFailsError(por raise eestar dentro except AlsoFailsError). Isso é enganoso porque o que realmente aconteceu é o contrário - encontramos AlsoFailsErrore lidamos com isso, enquanto tentávamos nos recuperar SomeError. Para obter um traceback que não inclua AlsoFailsError, substitua raise epor raise e from None.

No Python 2 você armazenaria o tipo de exceção, valor e traceback em variáveis ​​locais e usaria a forma de três argumentos deraise :

try:
    something()
except SomeError:
    t, v, tb = sys.exc_info()
    try:
        plan_B()
    except AlsoFailsError:
        raise t, v, tb
user4815162342
fonte
Perfeito, foi o que acabei de encontrar aqui , obrigado! Embora a sugestão seja raise self.exc_info[1], None, self.exc_info[2]depois de self.exc_info = sys.exc_info()- colocar [1]a primeira posição por algum motivo
Tobias Kienzler
3
@TobiasKienzler raise t, None, tbperderá o valor da exceção e forçará raiseuma nova instância do tipo, fornecendo a você um valor de exceção menos específico (ou simplesmente incorreto). Por exemplo, se a exceção levantada for KeyError("some-key"), ele apenas aumentará novamente KeyError()e omitirá a chave ausente exata do traceback.
user4815162342
3
@TobiasKienzler Ainda deve ser possível expressar isso no Python 3 como raise v.with_traceback(tb). (Seu comentário até diz isso, exceto que propõe
reinstanciar
2
Além disso, o aviso vermelho para não armazenar sys.exc_info()em uma variável local fazia sentido antes do Python 2.0 (lançado 13 anos atrás), mas beira o ridículo hoje. O Python moderno seria quase inútil sem o coletor de ciclo, já que toda biblioteca Python não trivial cria ciclos sem pausa e depende de sua limpeza correta.
user4815162342
1
@ user4815162342 Você pode eliminar o erro aninhado "outro erro ocorreu" escrevendo "raise e from None".
Matthias Urlichs
19

Mesmo que a solução aceita esteja certa, é bom apontar para a biblioteca Six que possui uma solução Python 2 + 3, usando six.reraise.

seis. reraise ( exc_type , exc_value , exc_traceback = Nenhum)

Reraise uma exceção, possivelmente com um traceback diferente. [...]

Então, você pode escrever:

import six


try:
    something()
except SomeError:
    t, v, tb = sys.exc_info()
    try:
        plan_B()
    except AlsoFailsError:
        six.reraise(t, v, tb)
Laurent LAPORTE
fonte
1
Bom ponto - por falar em Seis, você também pode usar six.raise_fromse quiser incluir informações que plan_B()também falharam.
Tobias Kienzler
1
@TobiasKienzler: Eu acho que é um uso diferente: com six.raise_fromvocê cria uma nova exceção que está ligada a uma anterior, você não levanta novamente , então o rastreamento de volta é diferente.
Laurent LAPORTE
1
Meu ponto exatamente - se você reraisetem a impressão apenas something()jogou SomeError, se raise_fromvocê também sabe que isso causou plan_B()ser executado, mas jogando o AlsoFailsError. Portanto, depende do caso de uso. Acho que raise_fromtornará a depuração mais fácil
Tobias Kienzler
9

Conforme a sugestão de Drew McGowen , mas cuidando de um caso geral (onde um valor de retorno sestá presente), aqui está uma alternativa para a resposta do usuário 4815162342 :

try:
    s = something()
except SomeError as e:
    def wrapped_plan_B():
        try:
            return False, plan_B()
        except:
            return True, None
    failed, s = wrapped_plan_B()
    if failed:
        raise
Tobias Kienzler
fonte
1
O bom dessa abordagem é que ela funciona inalterada no Python 2 e 3.
user4815162342
2
@ user4815162342 Bom argumento :) Embora enquanto isso no Python3 eu considerasse raise from, o rastreamento de pilha também me permitiria se o plano B falhou. Que pode ser emulado no Python 2 por sinal.
Tobias Kienzler
5

Python 3.5+ anexa as informações de traceback ao erro de qualquer maneira, então não é mais necessário salvá-lo separadamente.

>>> def f():
...   try:
...     raise SyntaxError
...   except Exception as e:
...     err = e
...     try:
...       raise AttributeError
...     except Exception as e1:
...       raise err from None
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in f
  File "<stdin>", line 3, in f
SyntaxError: None
>>> 
Matthias Urlichs
fonte
2
A questão é sobre outra exceção acontecendo durante o except. Mas você está certo, quando eu substituo err = epor, digamos,, raise AttributeErrorvocê obtém primeiro o SyntaxErrorrastreamento da pilha, seguido por ae o rastreamento During handling of the above exception, another exception occurred:da AttributeErrorpilha. É bom saber, embora, infelizmente, não se possa contar com a instalação do 3.5+. PS: ff verstehen nicht-Deutsche vermutlich nicht;)
Tobias Kienzler
OK, então mudei o exemplo para levantar outra exceção, que (como a pergunta original feita) é ignorada quando eu levanto a primeira novamente.
Matthias Urlichs
3
@TobiasKienzler 3.5+ (para o qual eu mudei) parece ser um formato reconhecido globalmente. Was denkst du? ;)
linusg
@linusg aprovado :)
Tobias Kienzler