Добавление информации в исключение?

150

Я хочу добиться чего-то вроде этого:

def foo():
   try:
       raise IOError('Stuff ')
   except:
       raise

def bar(arg1):
    try:
       foo()
    except Exception as e:
       e.message = e.message + 'happens at %s' % arg1
       raise

bar('arg1')
Traceback...
  IOError('Stuff Happens at arg1')

Но вот что я получаю:

Traceback..
  IOError('Stuff')

Есть какие-нибудь подсказки о том, как этого добиться? Как это сделать в Python 2 и 3?

Anijhaw
источник
1
При поиске документации для messageатрибута Exception я обнаружил этот вопрос SO, BaseException.message устарел в Python 2.6 , что, похоже, указывает на то, что его использование теперь не рекомендуется (и почему его нет в документации).
Мартино
к сожалению, эта ссылка больше не работает.
Майкл Скотт Катберт
1
@MichaelScottCuthbert, вот хорошая альтернатива: itmaybeahack.com/book/python-2.6/html/p02/…
Niels Keurentjes
Вот действительно хорошее объяснение того, каков статус атрибута сообщения и его связь с атрибутом args и PEP 352 . Это из бесплатной книги Стивена Ф. Лотта «Развитие навыков на языке Python ».
Мартино

Ответы:

125

Я бы сделал это так, чтобы изменение его типа foo()не потребовало также его изменения bar().

def foo():
    try:
        raise IOError('Stuff')
    except:
        raise

def bar(arg1):
    try:
        foo()
    except Exception as e:
        raise type(e)(e.message + ' happens at %s' % arg1)

bar('arg1')

Traceback (most recent call last):
  File "test.py", line 13, in <module>
    bar('arg1')
  File "test.py", line 11, in bar
    raise type(e)(e.message + ' happens at %s' % arg1)
IOError: Stuff happens at arg1

Обновление 1

Вот небольшая модификация, которая сохраняет исходную трассировку:

...
def bar(arg1):
    try:
        foo()
    except Exception as e:
        import sys
        raise type(e), type(e)(e.message +
                               ' happens at %s' % arg1), sys.exc_info()[2]

bar('arg1')

Traceback (most recent call last):
  File "test.py", line 16, in <module>
    bar('arg1')
  File "test.py", line 11, in bar
    foo()
  File "test.py", line 5, in foo
    raise IOError('Stuff')
IOError: Stuff happens at arg1

Обновление 2

Для Python 3.x код в моем первом обновлении синтаксически неверен, плюс идея наличия messageатрибута BaseExceptionбыла отозвана при изменении PEP 352 16 мая 2012 г. (мое первое обновление было опубликовано 12 марта 2012 г.) . Итак, в настоящее время в Python 3.5.2 вам все равно нужно будет что-то сделать в этих строках, чтобы сохранить трассировку, а не жестко кодировать тип исключения в функции bar(). Также обратите внимание, что там будет строка:

During handling of the above exception, another exception occurred:

в отображаемых сообщениях трассировки.

# for Python 3.x
...
def bar(arg1):
    try:
        foo()
    except Exception as e:
        import sys
        raise type(e)(str(e) +
                      ' happens at %s' % arg1).with_traceback(sys.exc_info()[2])

bar('arg1')

Обновление 3

Комментатор спросил, есть ли способ , который будет работать как в Python 2 и 3. Несмотря на то, что ответ может показаться, что «Нет» из - за различия синтаксиса, то есть способ обойти это, используя вспомогательную функцию как reraise()в sixAdd- на модуле. Итак, если вы по какой-то причине не хотите использовать библиотеку, ниже представлена ​​упрощенная автономная версия.

Также обратите внимание на то, что, поскольку исключение повторно возникает внутри reraise()функции, оно будет появляться при любом возбуждении трассировки, но конечный результат - это то, что вы хотите.

import sys

if sys.version_info.major < 3:  # Python 2?
    # Using exec avoids a SyntaxError in Python 3.
    exec("""def reraise(exc_type, exc_value, exc_traceback=None):
                raise exc_type, exc_value, exc_traceback""")
else:
    def reraise(exc_type, exc_value, exc_traceback=None):
        if exc_value is None:
            exc_value = exc_type()
        if exc_value.__traceback__ is not exc_traceback:
            raise exc_value.with_traceback(exc_traceback)
        raise exc_value

def foo():
    try:
        raise IOError('Stuff')
    except:
        raise

def bar(arg1):
    try:
       foo()
    except Exception as e:
        reraise(type(e), type(e)(str(e) +
                                 ' happens at %s' % arg1), sys.exc_info()[2])

bar('arg1')
Мартино
источник
3
Это теряет обратную трассировку, как бы побеждая точку добавления информации к существующему исключению. Кроме того, он не работает с исключениями с ctor, который принимает> 1 аргумент (тип - это то, что вы не можете контролировать с того места, где вы поймали исключение).
Václav Slavík
1
@ Вацлав: Довольно легко предотвратить потерю трассировки - как показано в добавленном мной обновлении. Хотя это по-прежнему не обрабатывает все мыслимые исключения, он работает для случаев, подобных тем, которые были показаны в вопросе OP.
Мартино,
1
Это не совсем так . Если тип (e) отменяет __str__, вы можете получить нежелательные результаты. Также обратите внимание, что второй аргумент передается конструктору, заданному первым аргументом, что дает несколько бессмысленный результат type(e)(type(e)(e.message). В-третьих, e.message устарел в пользу e.args [0].
букзор
1
Итак, нет портативного способа, который работал бы как в Python 2, так и в 3?
Элиас Дорнелес
1
@martineau Какова цель импорта внутри блока except? Это для экономии памяти за счет импорта только при необходимости?
AllTradesJack
132

Если вы пришли сюда в поисках решения для Python 3, в руководстве говорится:

При создании нового исключения (вместо использования простого raiseдля повторного вызова исключения, которое в настоящее время обрабатывается), неявный контекст исключения может быть дополнен явной причиной, используя fromwith raise:

raise new_exc from original_exc

Пример:

try:
    return [permission() for permission in self.permission_classes]
except TypeError as e:
    raise TypeError("Make sure your view's 'permission_classes' are iterable. "
                    "If you use '()' to generate a set with a single element "
                    "make sure that there is a comma behind the one (element,).") from e

Что в итоге выглядит так:

2017-09-06 16:50:14,797 [ERROR] django.request: Internal Server Error: /v1/sendEmail/
Traceback (most recent call last):
File "venv/lib/python3.4/site-packages/rest_framework/views.py", line 275, in get_permissions
    return [permission() for permission in self.permission_classes]
TypeError: 'type' object is not iterable 

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

Traceback (most recent call last):
    # Traceback removed...
TypeError: Make sure your view's Permission_classes are iterable. If 
     you use parens () to generate a set with a single element make 
     sure that there is a (comma,) behind the one element.

Превращение совершенно невзрачного сообщения TypeErrorв красивое сообщение с намеками на решение без нарушения исходного исключения.

Крис
источник
16
Это лучшее решение, поскольку полученное исключение указывает на исходную причину, предоставьте более подробную информацию.
JT
есть ли какое-нибудь решение, в котором мы можем добавить какое-то сообщение, но все же не вызвать новое исключение? Я имею в виду просто расширить сообщение экземпляра исключения.
edcSam
Да ~~, это работает, но такое чувство, что со мной не следует поступать. Сообщение хранится в e.args, но это кортеж, поэтому его нельзя изменить. Итак, сначала скопируйте argsв список, затем измените его, а затем скопируйте обратно как кортеж:args = list(e.args) args[0] = 'bar' e.args = tuple(args)
Крис
30

Предполагая, что вы не хотите или не можете изменять foo (), вы можете сделать это:

try:
    raise IOError('stuff')
except Exception as e:
    if len(e.args) >= 1:
        e.args = (e.args[0] + ' happens',) + e.args[1:]
    raise

Это действительно единственное решение, которое решает проблему в Python 3 без уродливого и запутанного сообщения «Во время обработки вышеуказанного исключения произошло другое исключение».

В случае, если необходимо добавить строку повторного повышения в трассировку стека, запись raise eвместо raiseэтого сделает свое дело.

Стив Ховард
источник
но в этом случае, если исключение изменится в foo, я должен также изменить bar, верно?
anijhaw
1
Если вы перехватываете Exception (отредактировано выше), вы можете перехватить любое исключение стандартной библиотеки (а также те, которые наследуются от Exception и вызывают Exception .__ init__).
Стив Ховард
6
чтобы быть более полными / совместными, e.args = ('mynewstr' + e.args[0],) + e.args[1:]
включите
1
@ nmz787 На самом деле это лучшее решение для Python 3. В чем именно заключается ваша ошибка?
Christian
1
@ Дубслоу и Мартино Я включил ваши предложения в правку.
Christian
9

Мне пока не нравятся все ответы. Они по-прежнему слишком многословны, имхо. В любом коде и выводе сообщения.

Все, что я хочу иметь, - это трассировка стека, указывающая на исходное исключение, без каких-либо исключений между ними, поэтому не нужно создавать новые исключения, просто повторно поднимая оригинал со всеми соответствующими состояниями фрейма стека в нем, что привело к этому.

Стив Ховард дал хороший ответ, который я хочу расширить, нет, уменьшить ... только до Python 3.

except Exception as e:
    e.args = ("Some failure state", *e.args)
    raise

Единственное новшество - это расширение / распаковка параметров, что делает его небольшим и достаточно простым в использовании.

Попытайся:

foo = None

try:
    try:
        state = "bar"
        foo.append(state)

    except Exception as e:
        e.args = ("Appending '"+state+"' failed", *e.args)
        raise

    print(foo[0]) # would raise too

except Exception as e:
    e.args = ("print(foo) failed: " + str(foo), *e.args)
    raise

Это даст вам:

Traceback (most recent call last):
  File "test.py", line 6, in <module>
    foo.append(state)
AttributeError: ('print(foo) failed: None', "Appending 'bar' failed", "'NoneType' object has no attribute 'append'")

Простой симпатичный принт может быть чем-то вроде

print("\n".join( "-"*i+" "+j for i,j in enumerate(e.args)))
6EQUJ5
источник
Обратной стороной этого является то, что внешние кадры не являются частью трассировки стека.
moi
5

Один удобный подход, который я использовал, - использовать атрибут класса в качестве хранилища деталей, поскольку атрибут класса доступен как из объекта класса, так и из экземпляра класса:

class CustomError(Exception):
    def __init__(self, details: Dict):
        self.details = details

Затем в вашем коде:

raise CustomError({'data': 5})

И при отлове ошибки:

except CustomError as e:
    # Do whatever you want with the exception instance
    print(e.details)
Ки
источник
Не очень полезно, поскольку OP запрашивает, чтобы детали были напечатаны как часть трассировки стека, когда исходное исключение выбрасывается и не перехвачено.
Cowbert
Думаю, решение хорошее. Но описание не соответствует действительности. Атрибуты класса копируются в экземпляры при их создании. Поэтому, когда вы изменяете атрибут «подробности» экземпляра, атрибут класса все равно будет None. В любом случае мы хотим здесь такого поведения.
Адам
3

Я предоставлю фрагмент кода, который я часто использую, когда хочу добавить дополнительную информацию к исключению. Я работаю на Python 2.7 и 3.6.

import sys
import traceback

try:
    a = 1
    b = 1j

    # The line below raises an exception because
    # we cannot compare int to complex.
    m = max(a, b)  

except Exception as ex:
    # I create my  informational message for debugging:
    msg = "a=%r, b=%r" % (a, b)

    # Gather the information from the original exception:
    exc_type, exc_value, exc_traceback = sys.exc_info()

    # Format the original exception for a nice printout:
    traceback_string = ''.join(traceback.format_exception(
        exc_type, exc_value, exc_traceback))

    # Re-raise a new exception of the same class as the original one, 
    # using my custom message and the original traceback:
    raise type(ex)("%s\n\nORIGINAL TRACEBACK:\n\n%s\n" % (msg, traceback_string))

Приведенный выше код приводит к следующему результату:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-6-09b74752c60d> in <module>()
     14     raise type(ex)(
     15         "%s\n\nORIGINAL TRACEBACK:\n\n%s\n" %
---> 16         (msg, traceback_string))

TypeError: a=1, b=1j

ORIGINAL TRACEBACK:

Traceback (most recent call last):
  File "<ipython-input-6-09b74752c60d>", line 7, in <module>
    m = max(a, b)  # Cannot compare int to complex
TypeError: no ordering relation is defined for complex numbers


Я знаю, что это немного отличается от примера, приведенного в вопросе, но тем не менее я надеюсь, что кто-то сочтет это полезным.

Педро М. Дуарте
источник
2

В отличие от предыдущих ответов, это работает с очень плохими исключениями __str__. Однако он изменяет тип, чтобы исключить бесполезные __str__реализации.

Я все еще хотел бы найти дополнительное улучшение, которое не изменяет тип.

from contextlib import contextmanager
@contextmanager
def helpful_info():
    try:
        yield
    except Exception as e:
        class CloneException(Exception): pass
        CloneException.__name__ = type(e).__name__
        CloneException.__module___ = type(e).__module__
        helpful_message = '%s\n\nhelpful info!' % e
        import sys
        raise CloneException, helpful_message, sys.exc_traceback


class BadException(Exception):
    def __str__(self):
        return 'wat.'

with helpful_info():
    raise BadException('fooooo')

Исходная трассировка и тип (имя) сохраняются.

Traceback (most recent call last):
  File "re_raise.py", line 20, in <module>
    raise BadException('fooooo')
  File "/usr/lib64/python2.6/contextlib.py", line 34, in __exit__
    self.gen.throw(type, value, traceback)
  File "re_raise.py", line 5, in helpful_info
    yield
  File "re_raise.py", line 20, in <module>
    raise BadException('fooooo')
__main__.BadException: wat.

helpful info!
букзор
источник
1

Вы можете определить собственное исключение, которое наследуется от другого, и создать собственный конструктор для установки значения.

Например:

class MyError(Exception):
   def __init__(self, value):
     self.value = value
     Exception.__init__(self)

   def __str__(self):
     return repr(self.value)
Александр Киселев
источник
2
Не требует изменения / добавления чего-либо к messageисходному исключению (но, я думаю, это можно исправить).
Мартино
-6

Может быть

except Exception as e:
    raise IOError(e.message + 'happens at %s'%arg1)
Мальволио
источник