"Exceção interna" (com rastreamento) em Python?


146

Minha formação é em C # e eu recentemente comecei a programar em Python. Quando uma exceção é lançada, normalmente quero agrupá-la em outra exceção que adiciona mais informações, enquanto ainda mostra o rastreamento completo da pilha. É muito fácil em C #, mas como faço em Python?

Por exemplo. em C #, eu faria algo assim:

try
{
  ProcessFile(filePath);
}
catch (Exception ex)
{
  throw new ApplicationException("Failed to process file " + filePath, ex);
}

No Python, posso fazer algo semelhante:

try:
  ProcessFile(filePath)
except Exception as e:
  raise Exception('Failed to process file ' + filePath, e)

... mas isso perde o rastro da exceção interna!

Editar: eu gostaria de ver as mensagens de exceção e os rastreamentos de pilha e correlacionar os dois. Ou seja, quero ver na saída que a exceção X ocorreu aqui e depois a exceção Y lá - o mesmo que faria em C #. Isso é possível no Python 2.6? Parece que o melhor que posso fazer até agora (com base na resposta de Glenn Maynard) é:

try:
  ProcessFile(filePath)
except Exception as e:
  raise Exception('Failed to process file' + filePath, e), None, sys.exc_info()[2]

Isso inclui as mensagens e os tracebacks, mas não mostra qual exceção ocorreu no traceback.


3
A resposta aceita está ficando desatualizada; talvez você deva considerar aceitar outra.
Aaron Hall

1
@AaronHall, infelizmente, o OP não é visto desde 2015.
Antti Haapala

Respostas:


136

Python 2

É simples; passar o retorno como o terceiro argumento a ser levantado.

import sys
class MyException(Exception): pass

try:
    raise TypeError("test")
except TypeError, e:
    raise MyException(), None, sys.exc_info()[2]

Sempre faça isso ao capturar uma exceção e aumentar novamente outra.


4
Obrigado. Isso preserva o retorno, mas perde a mensagem de erro da exceção original. Como vejo as mensagens e os dois rastreamentos?
EMP

6
raise MyException(str(e)), ..., etc
Glenn Maynard

23
Python 3 adiciona raise E() from tbe.with_traceback(...)
Dima Tisnek

3
@ GlennMaynard, é uma pergunta bastante antiga, mas o argumento do meio raiseé o valor a ser passado para a exceção (caso o primeiro argumento seja uma classe de exceção e não uma instância). Então, se você quer exceções de swap, em vez de fazer raise MyException(str(e)), None, sys.exc_info()[2], é melhor usar este: raise MyException, e.args, sys.exc_info()[2].
bgusach

8
Uma maneira compatível com Python2 e 3 é possível usando o pacote futuro: python-future.org/compatible_idioms.html#raising-exceptions Por exemplo, from future.utils import raise_e raise_(ValueError, None, sys.exc_info()[2]).
Jtpereyda

239

Python 3

No python 3, você pode fazer o seguinte:

try:
    raise MyExceptionToBeWrapped("I have twisted my ankle")

except MyExceptionToBeWrapped as e:

    raise MyWrapperException("I'm not in a good shape") from e

Isso produzirá algo como isto:

   Traceback (most recent call last):
   ...
   MyExceptionToBeWrapped: ("I have twisted my ankle")

The above exception was the direct cause of the following exception:

   Traceback (most recent call last):
   ...
   MyWrapperException: ("I'm not in a good shape")

17
raise ... from ...é realmente a maneira correta de fazer isso no Python 3. Isso precisa de mais votos.
Nakedible

NakedibleEu acho que é porque, infelizmente, a maioria das pessoas ainda não está usando Python 3.
Tim Ludwinski

Isso parece acontecer mesmo com o uso de 'from' no python 3
Steve Vermeulen

Pode ser portado para o Python 2. Espero que um dia.
Marcin Wojnarski

4
@ogrisel Você pode usar o futurepacote para fazer isso: python-future.org/compatible_idioms.html#raising-exceptions Por exemplo, from future.utils import raise_e raise_(ValueError, None, sys.exc_info()[2]).
jtpereyda

19

O Python 3 possui a cláusula raise...from para encadear exceções. A resposta de Glenn é ótima para o Python 2.7, mas ele usa apenas o rastreio da exceção original e joga fora a mensagem de erro e outros detalhes. Aqui estão alguns exemplos no Python 2.7 que adicionam informações de contexto do escopo atual à mensagem de erro da exceção original, mas mantêm outros detalhes intactos.

Tipo de exceção conhecido

try:
    sock_common = xmlrpclib.ServerProxy(rpc_url+'/common')
    self.user_id = sock_common.login(self.dbname, username, self.pwd)
except IOError:
    _, ex, traceback = sys.exc_info()
    message = "Connecting to '%s': %s." % (config['connection'],
                                           ex.strerror)
    raise IOError, (ex.errno, message), traceback

Esse tipo de raiseinstrução usa o tipo de exceção como a primeira expressão, os argumentos do construtor da classe de exceção em uma tupla como a segunda expressão e o retorno como a terceira expressão. Se você estiver executando antes do Python 2.2, consulte os avisos em sys.exc_info().

Qualquer tipo de exceção

Aqui está outro exemplo de propósito mais geral, se você não souber que tipo de exceção seu código pode ter que capturar. A desvantagem é que ele perde o tipo de exceção e apenas gera um RuntimeError. Você precisa importar o tracebackmódulo.

except Exception:
    extype, ex, tb = sys.exc_info()
    formatted = traceback.format_exception_only(extype, ex)[-1]
    message = "Importing row %d, %s" % (rownum, formatted)
    raise RuntimeError, message, tb

Modifique a mensagem

Aqui está outra opção se o tipo de exceção permitir que você adicione contexto a ele. Você pode modificar a mensagem da exceção e, em seguida, aumentá-la.

import subprocess

try:
    final_args = ['lsx', '/home']
    s = subprocess.check_output(final_args)
except OSError as ex:
    ex.strerror += ' for command {}'.format(final_args)
    raise

Isso gera o seguinte rastreamento de pilha:

Traceback (most recent call last):
  File "/mnt/data/don/workspace/scratch/scratch.py", line 5, in <module>
    s = subprocess.check_output(final_args)
  File "/usr/lib/python2.7/subprocess.py", line 566, in check_output
    process = Popen(stdout=PIPE, *popenargs, **kwargs)
  File "/usr/lib/python2.7/subprocess.py", line 710, in __init__
    errread, errwrite)
  File "/usr/lib/python2.7/subprocess.py", line 1327, in _execute_child
    raise child_exception
OSError: [Errno 2] No such file or directory for command ['lsx', '/home']

Você pode ver que ele mostra a linha onde check_output()foi chamada, mas a mensagem de exceção agora inclui a linha de comando.


1
De onde ex.strerrorvem isso? Não consigo encontrar nenhum resultado relevante nos documentos do Python. Não deveria ser str(ex)?
Henrik Heimbuerger

1
IOErroré derivado de EnvironmentError, @hheimbuerger, que proporciona o errornoe strerroratributos.
Don Kirkby

Como envolveria um arbitrário Error, por exemplo, ValueError, em uma RuntimeErrorcaptura Exception? Se eu reproduzir sua resposta para este caso, o rastreamento de pilha será perdido.
Karl Richter

Não sei bem o que você está perguntando, @karl. Você pode postar uma amostra em uma nova pergunta e depois vinculá-la a partir daqui?
Don Kirkby

Editei minha duplicata da pergunta do OP em stackoverflow.com/questions/23157766/… com um esclarecimento, levando em consideração sua resposta diretamente. Devemos discutir lá :)
Karl Richter

12

No Python 3.x :

raise Exception('Failed to process file ' + filePath).with_traceback(e.__traceback__)

ou simplesmente

except Exception:
    raise MyException()

que será propagado, MyExceptionmas imprimirá as duas exceções, se não for tratado.

No Python 2.x :

raise Exception, 'Failed to process file ' + filePath, e

Você pode impedir a impressão das duas exceções, matando o __context__atributo. Aqui, escrevo um gerenciador de contexto usando isso para capturar e alterar sua exceção rapidamente : (consulte http://docs.python.org/3.1/library/stdtypes.html para saber como eles funcionam)

try: # Wrap the whole program into the block that will kill __context__.

    class Catcher(Exception):
        '''This context manager reraises an exception under a different name.'''

        def __init__(self, name):
            super().__init__('Failed to process code in {!r}'.format(name))

        def __enter__(self):
            return self

        def __exit__(self, exc_type, exc_val, exc_tb):
            if exc_type is not None:
                self.__traceback__ = exc_tb
                raise self

    ...


    with Catcher('class definition'):
        class a:
            def spam(self):
                # not really pass, but you get the idea
                pass

            lut = [1,
                   3,
                   17,
                   [12,34],
                   5,
                   _spam]


        assert a().lut[-1] == a.spam

    ...


except Catcher as e:
    e.__context__ = None
    raise

4
TypeError: raise: arg 3 deve ser um retorno ou Nenhum
Glenn Maynard

Desculpe, cometi um erro, de alguma forma, pensei que ele também aceita exceções e obtém o atributo traceback automaticamente. Conforme docs.python.org/3.1/reference/… , deve ser e .__ traceback__
ilya n.

1
@ilyan .: Python 2 não tem e.__traceback__atributo!
Jan Hudec

5

Eu não acho que você possa fazer isso no Python 2.x, mas algo semelhante a essa funcionalidade faz parte do Python 3. No PEP 3134 :

Na implementação atual do Python, as exceções são compostas de três partes: o tipo, o valor e o retorno. O módulo 'sys' expõe a exceção atual em três variáveis ​​paralelas, exc_type, exc_value e exc_traceback, a função sys.exc_info () retorna uma tupla dessas três partes e a instrução 'raise' tem uma forma de três argumentos que aceita estas três partes. Manipular exceções geralmente requer a passagem dessas três coisas em paralelo, o que pode ser entediante e propenso a erros. Além disso, a instrução 'exceto' pode fornecer acesso apenas ao valor, não ao traceback. Adicionar o atributo ' traceback ' a valores de exceção torna todas as informações de exceção acessíveis em um único local.

Comparação com C #:

Exceções em C # contêm uma propriedade 'InnerException' somente leitura que pode apontar para outra exceção. Sua documentação [10] diz que "Quando uma exceção X é lançada como resultado direto de uma exceção anterior Y, a propriedade InnerException de X deve conter uma referência a Y." Esta propriedade não é configurada pela VM automaticamente; todos os construtores de exceção usam um argumento opcional 'innerException' para defini-lo explicitamente. O atributo ' cause ' cumpre o mesmo objetivo que InnerException, mas esse PEP propõe uma nova forma de 'raise' em vez de estender os construtores de todas as exceções. C # também fornece um método GetBaseException que salta diretamente para o final da cadeia InnerException;

Observe também que Java, Ruby e Perl 5 também não suportam esse tipo de coisa. Citando novamente:

Quanto a outras linguagens, Java e Ruby descartam a exceção original quando outra exceção ocorre em uma cláusula 'catch' / 'rescue' ou 'finalmente' / 'assegura'. O Perl 5 não possui tratamento de exceção estruturado interno. Para o Perl 6, o RFC número 88 [9] propõe um mecanismo de exceção que retém implicitamente as exceções encadeadas em uma matriz chamada @@.


Mas, é claro, no Perl5 você pode simplesmente dizer "confessar qq {OH NOES! $ @}" E não perder o rastreamento de pilha da outra exceção. Ou você pode implementar seu próprio tipo que mantém a exceção.
jrockway

4

Para máxima compatibilidade entre Python 2 e 3, você pode usar raise_fromna sixbiblioteca. https://six.readthedocs.io/#six.raise_from . Aqui está o seu exemplo (ligeiramente modificado para maior clareza):

import six

try:
  ProcessFile(filePath)
except Exception as e:
  six.raise_from(IOError('Failed to process file ' + repr(filePath)), e)

3

Você pode usar minha classe CausedException para encadear exceções no Python 2.x (e mesmo no Python 3 pode ser útil caso você queira fornecer mais de uma exceção capturada como causa de uma exceção recém-criada). Talvez possa ajudá-lo.


2

Talvez você possa pegar as informações relevantes e passá-las? Estou pensando em algo como:

import traceback
import sys
import StringIO

class ApplicationError:
    def __init__(self, value, e):
        s = StringIO.StringIO()
        traceback.print_exc(file=s)
        self.value = (value, s.getvalue())

    def __str__(self):
        return repr(self.value)

try:
    try:
        a = 1/0
    except Exception, e:
        raise ApplicationError("Failed to process file", e)
except Exception, e:
    print e

2

Assumindo:

  • você precisa de uma solução, que funcione para o Python 2 (para o Python 3 puro, consulte a raise ... fromsolução)
  • apenas deseja enriquecer a mensagem de erro, por exemplo, fornecendo algum contexto adicional
  • precisa do rastreamento completo da pilha

você pode usar uma solução simples no https://docs.python.org/3/tutorial/errors.html#raising-exceptions dos documentos :

try:
    raise NameError('HiThere')
except NameError:
    print 'An exception flew by!' # print or log, provide details about context
    raise # reraise the original exception, keeping full stack trace

A saída:

An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in ?
NameError: HiThere

Parece que a peça principal é a palavra-chave simplificada 'raise' que fica sozinha. Isso aumentará novamente a exceção no bloco de exceção.


Esta é a solução compatível com Python 2 e 3! Obrigado!
Andy Chase '

Penso que a ideia era criar um tipo diferente de exceção.
Tim Ludwinski

2
Esta não é uma cadeia de exceções aninhadas, apenas reraise uma exceção
Karl Richter

Esta é a melhor solução do python 2, se você apenas precisar enriquecer a mensagem de exceção e ter o rastreamento da pilha completa!
precisa saber é

Qual é a diferença entre usar raise e raise de
variável
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.