Трассировка исключений скрыта, если не вызывается немедленно

У меня есть кусок кода, похожий на этот:

import sys

def func1():
    func2()

def func2():
    raise Exception('test error')

def main():
    err = None

    try:
        func1()
    except:
        err = sys.exc_info()[1]
        pass

    # some extra processing, involving checking err details (if err is not None)

    # need to re-raise err so caller can do its own handling
    if err:
        raise err

if __name__ == '__main__':
    main()

Когда func2 вызывает исключение, я получаю следующую трассировку:

Traceback (most recent call last):
  File "err_test.py", line 25, in <module>
    main()
  File "err_test.py", line 22, in main
    raise err
Exception: test error

Отсюда я не вижу, откуда исходит исключение. Исходная трассировка потеряна.

Как я могу сохранить исходную трассировку и повторно поднять ее? Я хочу увидеть что-то похожее на это:

Traceback (most recent call last):
  File "err_test.py", line 26, in <module>
    main()
  File "err_test.py", line 13, in main
    func1()
  File "err_test.py", line 4, in func1
    func2()
  File "err_test.py", line 7, in func2
    raise Exception('test error')
Exception: test error

person parxier    schedule 28.01.2011    source источник


Ответы (5)


Пустой raise вызывает последнее исключение.

# need to re-raise err so caller can do its own handling
if err:
    raise

Если вы используете raise something, у Python нет возможности узнать, было ли something исключением, только что пойманным ранее, или новым исключением с новой трассировкой стека. Вот почему есть пустой raise, который сохраняет трассировку стека.

Ссылка здесь

person Jochen Ritzel    schedule 28.01.2011
comment
Стоит отметить, что это не работает в Python 3. - person yprez; 14.12.2015
comment
Это в комментарии yprez означает пустой рейз после выхода из блока кроме. Простое повышение работает в Python 3 (но только внутри блока исключений). - person jtniehof; 19.05.2016

Исключение можно изменить и повторно генерировать:

Если выражения отсутствуют, raise повторно вызывает последнее исключение, которое было активным в текущей области. Если в текущей области не активно ни одно исключение, возникает исключение TypeError, указывающее, что это ошибка (при работе в режиме IDLE вместо этого возникает исключение Queue.Empty).

В противном случае raise оценивает выражения для получения трех объектов, используя None в качестве значения пропущенных выражений. Первые два объекта используются для определения типа и значения исключения.

Если присутствует третий объект, а не None, это должен быть объект трассировки (см. раздел Стандартная иерархия типов), и он подставляется вместо текущего местоположения в качестве места возникновения исключения. Если присутствует третий объект, а не объект трассировки или None, возникает исключение TypeError.

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

Итак, если вы хотите изменить исключение и повторно выдать его, вы можете сделать это:

try:
    buggy_code_which_throws_exception()
except Exception as e:
    raise Exception, "The code is buggy: %s" % e, sys.exc_info()[2]
person qris    schedule 04.01.2013
comment
Интересный. Принятый ответ лучше обрабатывает вариант использования OP, но это интересно как более общий ответ. Я не вижу в этом особой пользы, поскольку трассировка действительно вводит в заблуждение, если вы поймаете, скажем, ValueError и поднимете RuntimeError (тогда вы не увидите, что ValueError когда-либо был задействован), и единственные случаи, которые я сталкивался лично, когда я хотел сохранить трассировку, но сделать что-то более сложное, чем просто raise без аргументов, были случаи, когда я хотел вызвать исключение другого типа. - person Mark Amery; 27.03.2013
comment
Я использовал это для повторного создания того же исключения с другим сообщением, включая дополнительные сведения об условиях, вызвавших исключение, которые доступны во внешней области, но не во внутренней. - person qris; 02.04.2013
comment
Иногда есть очень веские причины для использования этой формы повышения из трех выражений. Ваш ответ только что помог мне написать декоратор, который оборачивает интеграционные тесты и в случае неудачи делает снимок экрана. А затем поднимает исходное утверждение о сбое. Проблема заключалась в том, что трассировка была забита попытками/исключениями в коде создания снимка экрана. Тогда спасибо! - person aychedee; 17.03.2014
comment
есть ли способ сделать это совместимым с Python 2 и Python 3 способом? Я получаю SyntaxError в Python 3. - person Elias Dorneles; 13.04.2016
comment
@elias six.reraise(exc_type, exc_value, exc_traceback=None) - person qris; 15.04.2016

Вы можете получить много информации об исключении через sys.exc_info() вместе с модулем traceback.

попробуйте следующее расширение вашего кода.

import sys
import traceback

def func1():
    func2()

def func2():
    raise Exception('test error')

def main():

    try:
        func1()
    except:
        exc_type, exc_value, exc_traceback = sys.exc_info()
        # Do your verification using exc_value and exc_traceback

        print "*** print_exception:"
        traceback.print_exception(exc_type, exc_value, exc_traceback,
                                  limit=3, file=sys.stdout)

if __name__ == '__main__':
    main()

Это будет печатать, похожее на то, что вы хотели.

*** print_exception:
Traceback (most recent call last):
  File "err_test.py", line 14, in main
    func1()
  File "err_test.py", line 5, in func1
    func2()
  File "err_test.py", line 8, in func2
    raise Exception('test error')
Exception: test error
person Senthil Kumaran    schedule 28.01.2011
comment
Нет, я не хочу печатать это в main(). Я хочу повторно поднять его с исходной трассировкой и позволить вызывающей стороне main() обработать ее (например, игнорировать, печатать на консоли, сохранять в БД и т. д.). Решение Йохена сработало. - person parxier; 28.01.2011
comment
Это будет лучший ответ для Python3, если вы измените print на что-то вроде raise exc_type.with_traceback(exc_value, exc_traceback) - person Davos; 28.11.2018

Хотя ответ @Jochen хорошо работает в простом случае, он не способен обрабатывать более сложные случаи, когда вы не ловите и не перебрасываете напрямую, но по какой-то причине получаете исключение как объект и хотите повторно бросить полностью новый контекст (т. е. если вам нужно обработать его в другом процессе).

В этом случае предлагаю следующее:

  1. получить оригинал exc_info
  2. отформатировать исходное сообщение об ошибке с трассировкой стека
  3. создать новое исключение с этим полным сообщением об ошибке (включая трассировку стека), встроенным

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

class ChildTaskException(Exception):
    pass

В коде нарушения...

import sys
import traceback

try:
    # do something dangerous
except:
    error_type, error, tb = sys.exc_info()
    error_lines = traceback.format_exception(error_type, error, tb)
    error_msg = ''.join(error_lines)
    # for example, if you are doing multiprocessing, you might want to send this to another process via a pipe
    connection.send(error_msg)

Сбросить...

# again, a multiprocessing example of receiving that message through a pipe
error_msg = pcon.recv()
raise ChildTaskException(error_msg)
person tvt173    schedule 11.04.2017

Ваша основная функция должна выглядеть так:

def main():
    try:
        func1()
    except Exception, err:
        # error processing
        raise

Это стандартный способ обработки (и повторного вызова) ошибок. Это демонстрация кода.

person Gabi Purcaru    schedule 28.01.2011
comment
У меня есть ощущение, что except Exception, err: можно обойти с помощью raise "bad exception" старого стиля создания исключений. - person parxier; 28.01.2011
comment
@parxier, тогда используйте except object, err - person Gabi Purcaru; 28.01.2011
comment
Это ничем не отличается от err = sys.exc_info()[1]. В любом случае, основной задачей было повторно поднять err вне блока except без потери исходной трассировки. Решение Йохена сработало. - person parxier; 29.01.2011