Redigite a exceção com um tipo e mensagem diferentes, preservando as informações existentes

139

Estou escrevendo um módulo e quero ter uma hierarquia de exceções unificada para as exceções que ele pode gerar (por exemplo, herdar de uma FooErrorclasse abstrata para todas asfoo exceções específicas do módulo). Isso permite que os usuários do módulo capturem essas exceções específicas e as tratem distintamente, se necessário. Mas muitas das exceções levantadas no módulo são geradas por causa de alguma outra exceção; por exemplo, falha em alguma tarefa devido a um OSError em um arquivo.

O que eu preciso é "agrupar" a exceção capturada, de modo que ela tenha um tipo e mensagem diferentes , para que as informações estejam disponíveis ainda mais na hierarquia de propagação por qualquer que seja a captura da exceção. Mas não quero perder o tipo, a mensagem e o rastreamento de pilha existentes; todas essas informações são úteis para alguém que tenta depurar o problema. Um manipulador de exceção de nível superior não é bom, pois estou tentando decorar a exceção antes que ela avance na pilha de propagação, e o manipulador de nível superior está muito atrasado.

Isso é parcialmente resolvido derivando os footipos de exceção específicos do meu módulo do tipo existente (por exemplo class FooPermissionError(OSError, FooError)), mas isso não facilita a quebra da instância de exceção existente em um novo tipo, nem modifica a mensagem.

PEP 3134 do Python "Encadeamento de exceções e rastreamentos incorporados" discute uma alteração aceita no Python 3.0 para "encadear" objetos de exceção, para indicar que uma nova exceção foi gerada durante o tratamento de uma exceção existente.

O que estou tentando fazer está relacionado: preciso também que ele funcione em versões anteriores do Python, e não para encadeamento, mas apenas para polimorfismo. Qual é a maneira certa de fazer isso?

nariz grande
fonte
As exceções já são completamente polimórficas - todas são subclasses de Exceção. O que você está tentando fazer? "Mensagem diferente" é bastante trivial com um manipulador de exceção de nível superior. Por que você está mudando de classe?
315/09 S.Lott
Conforme explicado na pergunta (agora, obrigado pelo seu comentário): Estou tentando decorar uma exceção que peguei, para que ela possa se propagar mais com mais informações, mas sem perder nenhuma. Um manipulador de nível superior é tarde demais.
Bignose
uma olhada na minha classe CausedException, que pode fazer o que você deseja no Python 2.x. Também no Python 3, pode ser útil caso você queira fornecer mais de uma exceção original como causa da sua exceção. Talvez atenda às suas necessidades.
Alfe
Para python-2, faço algo semelhante ao @DevinJeanpierre, mas estou apenas anexando uma nova mensagem de string: except Exception as e-> raise type(e), type(e)(e.message + custom_message), sys.exc_info()[2]-> esta solução é de outra questão SO . Isso não é bonito, mas funcional.
Trevor Boyd Smith

Respostas:

197

O Python 3 introduziu o encadeamento de exceções (conforme descrito no PEP 3134 ). Isso permite, ao criar uma exceção, citar uma exceção existente como a "causa":

try:
    frobnicate()
except KeyError as exc:
    raise ValueError("Bad grape") from exc

A exceção capturada, assim, se torna parte (é a “causa”) da nova exceção e está disponível para qualquer código que captura a nova exceção.

Ao usar esse recurso, o __cause__atributo é definido. O manipulador de exceção interno também sabe como relatar a "causa" e o "contexto" da exceção junto com o rastreamento.


No Python 2 , parece que este caso de uso não tem uma boa resposta (conforme descrito por Ian Bicking e Ned Batchelder ). Vadio.

nariz grande
fonte
4
Ian Bicking não descreve minha solução? Lamento ter dado uma resposta tão boa, mas é estranho que essa tenha sido aceita.
Devin Jeanpierre
1
@bignose Você tem meu ponto não só de ser direita, mas para o uso de "frobnicate" :)
David M.
5
Exceção encadeamento é realmente o comportamento padrão agora, na verdade, é o oposto da questão, suprimindo a primeira exceção que exige trabalho, consulte PEP 409 python.org/dev/peps/pep-0409
Chris_Rands
1
Como você faria isso no python 2?
Selotape # 21/17
1
Parece funcionar bem (python 2,7)try: return 2 / 0 except ZeroDivisionError as e: raise ValueError(e)
alex
37

Você pode usar sys.exc_info () para obter o rastreamento e criar sua nova exceção com o referido rastreamento (como o PEP menciona). Se você deseja preservar o tipo e a mensagem antigos, pode fazê-lo na exceção, mas isso só será útil se o que detectar a exceção a procurar.

Por exemplo

import sys

def failure():
    try: 1/0
    except ZeroDivisionError, e:
        type, value, traceback = sys.exc_info()
        raise ValueError, ("You did something wrong!", type, value), traceback

Claro, isso realmente não é tão útil. Se fosse, não precisaríamos desse PEP. Eu não recomendaria fazê-lo.

Devin Jeanpierre
fonte
Devin, você armazena uma referência ao retorno, não deveria excluir explicitamente essa referência?
Arafangion
2
Não guardei nada, deixei o traceback como uma variável local que, presumivelmente, fica fora do escopo. Sim, é concebível que isso não ocorra, mas se você criar exceções como essa no escopo global e não nas funções, terá problemas maiores. Se a sua reclamação é apenas a de que ela pode ser executada em um escopo global, a solução adequada não é adicionar clichês irrelevantes que precisam ser explicados e que não sejam relevantes para 99% dos usos, mas reescrever a solução para que isso não ocorra. é necessário, fazendo parecer que nada é diferente - como eu fiz agora.
Devin Jeanpierre
4
Arafangion pode estar se referindo a um aviso na documentaçãosys.exc_info() do Python para @Devin. Ele diz: "Atribuir o valor de retorno do traceback a uma variável local em uma função que está manipulando uma exceção causará uma referência circular". No entanto, uma observação a seguir diz que, desde o Python 2.2, o ciclo pode ser limpo, mas é mais eficiente evitá-lo.
Don Kirkby
5
Mais detalhes sobre maneiras diferentes de exceções re-raise em Python de dois pythonistas iluminados: Ian Bicking e Ned Batchelder
Rodrigue
11

Você pode criar seu próprio tipo de exceção que estende a exceção que você capturou.

class NewException(CaughtException):
    def __init__(self, caught):
        self.caught = caught

try:
    ...
except CaughtException as e:
    ...
    raise NewException(e)

Mas na maioria das vezes, acho que seria mais simples capturar a exceção, manipulá-la e raisea exceção original (e preservar o retorno) ou raise NewException(). Se eu estivesse chamando seu código e recebesse uma de suas exceções personalizadas, esperaria que seu código já tenha tratado qualquer exceção que você teve que capturar. Portanto, eu não preciso acessá-lo pessoalmente.

Edit: Eu encontrei esta análise de maneiras de lançar sua própria exceção e manter a exceção original. Não há soluções bonitas.

Nikhil Chelliah
fonte
1
O caso de uso que descrevi não é para lidar com a exceção; trata-se especificamente de não tratá-lo, mas adicionando algumas informações extras (uma classe adicional e uma nova mensagem) para que possam ser manuseadas ainda mais na pilha de chamadas.
Bignose
2

Eu também descobri que muitas vezes eu preciso de algum "empacotamento" para os erros gerados.

Isso incluía o escopo de uma função e, às vezes, quebra apenas algumas linhas dentro de uma função.

Criou um wrapper para ser usado a decoratore context manager:


Implementação

import inspect
from contextlib import contextmanager, ContextDecorator
import functools    

class wrap_exceptions(ContextDecorator):
    def __init__(self, wrapper_exc, *wrapped_exc):
        self.wrapper_exc = wrapper_exc
        self.wrapped_exc = wrapped_exc

    def __enter__(self):
        pass

    def __exit__(self, exc_type, exc_val, exc_tb):
        if not exc_type:
            return
        try:
            raise exc_val
        except self.wrapped_exc:
            raise self.wrapper_exc from exc_val

    def __gen_wrapper(self, f, *args, **kwargs):
        with self:
            for res in f(*args, **kwargs):
                yield res

    def __call__(self, f):
        @functools.wraps(f)
        def wrapper(*args, **kw):
            with self:
                if inspect.isgeneratorfunction(f):
                    return self.__gen_wrapper(f, *args, **kw)
                else:
                    return f(*args, **kw)
        return wrapper

Exemplos de uso

decorador

@wrap_exceptions(MyError, IndexError)
def do():
   pass

ao chamar o dométodo, não se preocupe IndexError, apenasMyError

try:
   do()
except MyError as my_err:
   pass # handle error 

gerenciador de contexto

def do2():
   print('do2')
   with wrap_exceptions(MyError, IndexError):
       do()

dentro do2, no context manager, se IndexErrorfor levantado, será embrulhado e levantadoMyError

Aaron_ab
fonte
1
Por favor, explique o que "quebra automática" fará com a exceção original. Qual é o objetivo do seu código e qual comportamento ele permite?
Alexis
@alexis - acrescentou alguns exemplos, espero que ajude
Aaron_ab
-2

A solução mais direta para suas necessidades deve ser esta:

try:
     upload(file_id)
except Exception as upload_error:
     error_msg = "Your upload failed! File: " + file_id
     raise RuntimeError(error_msg, upload_error)

Dessa forma, você pode imprimir sua mensagem posteriormente e o erro específico gerado pela função de upload

funny_dev
fonte
1
Isso captura e depois joga fora o objeto de exceção; portanto, não, ele não atende às necessidades da pergunta. A pergunta pergunta como manter a exceção existente e permitir que a mesma exceção, com todas as informações úteis que ela contém, continue propagando a pilha.
Bignose