«Внутреннее исключение» (с отслеживанием) в Python?


147

Мой опыт работы в C #, и я только недавно начал программировать на Python. Когда выдается исключение, я обычно хочу обернуть его в другое исключение, которое добавляет больше информации, в то же время показывая полную трассировку стека. Это довольно легко в C #, но как мне это сделать в Python?

Например. в C # я бы сделал что-то вроде этого:

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

В Python я могу сделать нечто подобное:

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

... но это теряет след внутреннего исключения!

Изменить: я хотел бы видеть оба сообщения об исключениях и трассировки стека и коррелировать их. То есть я хочу видеть в выводе, что исключение X возникло здесь, а затем исключение Y там - так же, как и в C #. Возможно ли это в Python 2.6? Похоже, лучшее, что я могу сделать до сих пор (основываясь на ответе Гленна Мейнарда), это:

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

Это относится как к сообщениям, так и к обоим трассировкам, но не показывает, какое исключение произошло в трассировке.


3
Принятый ответ устарел, возможно, вам стоит подумать о принятии другого.
Аарон Холл

1
@AaronHall, к сожалению, OP не видели с 2015 года.
Антти Хаапала

Ответы:


137

Python 2

Это просто; передать в качестве третьего аргумента трассировку.

import sys
class MyException(Exception): pass

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

Всегда делайте это, когда ловите одно исключение и повторно вызываете другое.


4
Спасибо. Это сохраняет трассировку, но теряет сообщение об ошибке исходного исключения. Как я вижу оба сообщения и оба следа?
EMP

6
raise MyException(str(e)), ...и т. д.
Гленн Мейнард

23
Python 3 добавляет raise E() from tbи.with_traceback(...)
Дима Тиснек

3
@GlennMaynard это довольно старый вопрос, но средний аргумент raise- это значение, передаваемое исключению (в случае, если первый аргумент является классом исключения, а не экземпляром). Так что если вы хотите , чтобы исключения своп, а не делать raise MyException(str(e)), None, sys.exc_info()[2], то лучше использовать это: raise MyException, e.args, sys.exc_info()[2].
bgusach

8
Способ, совместимый с Python2 и 3, возможен при использовании будущего пакета: python-future.org/compatible_idioms.html#raising-exceptions Например, from future.utils import raise_и raise_(ValueError, None, sys.exc_info()[2]).
Jtpereyda

239

Python 3

В Python 3 вы можете сделать следующее:

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

except MyExceptionToBeWrapped as e:

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

Это будет производить что-то вроде этого:

   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 ...это действительно правильный способ сделать это в Python 3. Это требует больше голосов.
Nakedible

NakedibleЯ думаю, это потому, что, к сожалению, большинство людей до сих пор не используют Python 3.
Тим Людвински

Это, кажется, происходит даже с использованием 'from' в python 3
Steve Vermeulen

Может быть перенесен на Python 2. Надеюсь, что когда-нибудь.
Марчин Войнарски

4
@ogrisel Вы можете использовать futureпакет для достижения этой цели: python-future.org/compatible_idioms.html#raising-exceptions Например, from future.utils import raise_и raise_(ValueError, None, sys.exc_info()[2]).
Jtpereyda

19

Python 3 имеет raise... fromусловие для исключения цепи. Ответ Гленна хорош для Python 2.7, но он использует только трассировку исходного исключения и выбрасывает сообщение об ошибке и другие детали. Вот несколько примеров в Python 2.7, которые добавляют контекстную информацию из текущей области в сообщение об ошибке исходного исключения, но оставляют другие детали нетронутыми.

Известный тип исключения

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

Эта разновидность raiseоператора принимает тип исключения в качестве первого выражения, аргументы конструктора класса исключения в кортеже в качестве второго выражения и трассировку в качестве третьего выражения. Если вы работаете раньше, чем Python 2.2, см. Предупреждения на sys.exc_info().

Любой тип исключения

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

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

Изменить сообщение

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

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

Это генерирует следующую трассировку стека:

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']

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


1
Откуда ex.strerrorприходит? Я не могу найти какой-либо соответствующий хит для этого в документации по Python. Не должно ли это быть str(ex)?
Хенрик Хаймбюргер

1
IOErrorявляется производным от EnvironmentError, @hheimbuerger, который обеспечивает errornoи strerrorатрибуты.
Дон Киркби

Как бы я обернуть произвольный Error, например, ValueError, в RuntimeErrorперехват Exception? Если я воспроизведу ваш ответ для этого случая, трассировка стека будет потеряна.
Карл Рихтер

Я не уверен, что ты спрашиваешь, @karl. Можете ли вы опубликовать образец в новом вопросе, а затем ссылку на него отсюда?
Дон Киркби

Я отредактировал свой дубликат вопроса ОП на stackoverflow.com/questions/23157766/… с уточнением, учитывающим ваш ответ напрямую. Мы должны обсудить там :)
Карл Рихтер

12

В Python 3.x :

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

или просто

except Exception:
    raise MyException()

который будет распространяться, MyExceptionно печатать оба исключения, если это не будет обработано.

В Python 2.x :

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

Вы можете предотвратить печать обоих исключений, убив __context__атрибут. Здесь я пишу менеджер контекста с помощью, чтобы поймать и изменить исключение на лету (см http://docs.python.org/3.1/library/stdtypes.html для expanation того , как они работают)

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
Ошибка типа: повысить: arg 3 должно быть следом или нет
Гленн Мейнард

Извините, я допустил ошибку, так или иначе, я подумал, что он также принимает исключения и автоматически получает их атрибут traceback. Согласно docs.python.org/3.1/reference/… , это должно быть e .__ traceback__
ilya n.

1
@ilyan .: Python 2 не имеет e.__traceback__атрибута!
Ян Худек

5

Я не думаю, что вы можете сделать это в Python 2.x, но что-то похожее на эту функциональность является частью Python 3. Из PEP 3134 :

В сегодняшней реализации Python исключения состоят из трех частей: типа, значения и обратной трассировки. Модуль 'sys' отображает текущее исключение в трех параллельных переменных: exc_type, exc_value и exc_traceback, функция sys.exc_info () возвращает кортеж из этих трех частей, а оператор Повышение имеет форму с тремя аргументами, принимающую эти три части. Манипулирование исключениями часто требует параллельного прохождения этих трех вещей, что может быть утомительным и подверженным ошибкам. Кроме того, оператор «исключение» может предоставлять доступ только к значению, а не к трассировке. Добавление атрибута traceback к значениям исключений делает всю информацию об исключениях доступной из одного места.

Сравнение с C #:

Исключения в C # содержат доступное только для чтения свойство InnerException, которое может указывать на другое исключение. В документации [10] говорится, что «когда исключение X генерируется как прямой результат предыдущего исключения Y, свойство InnerException X должно содержать ссылку на Y». Это свойство не устанавливается виртуальной машиной автоматически; скорее, все конструкторы исключений принимают необязательный аргумент innerException, чтобы установить его явно. Атрибут ' причина ' выполняет ту же цель, что и InnerException, но этот PEP предлагает новую форму 'повышения', а не расширение конструкторов всех исключений. C # также предоставляет метод GetBaseException, который непосредственно переходит в конец цепочки InnerException;

Также обратите внимание, что Java, Ruby и Perl 5 также не поддерживают этот тип вещей. Цитирую еще раз:

Что касается других языков, то Java и Ruby оба отбрасывают исходное исключение, когда в предложении «catch» / «rescue» или «finally» / «sure» возникает другое исключение. В Perl 5 отсутствует встроенная структурированная обработка исключений. Для Perl 6 в RFC № 88 [9] предлагается механизм исключений, который неявно сохраняет цепочечные исключения в массиве с именем @@.


Но, конечно, в Perl5 вы можете просто сказать «признайтесь qq {OH NOES! $ @}» И не потерять трассировку стека другого исключения. Или вы можете реализовать свой собственный тип, который сохраняет исключение.
jrockway

4

Для максимальной совместимости между Python 2 и 3, вы можете использовать raise_fromв sixбиблиотеке. https://six.readthedocs.io/#six.raise_from . Вот ваш пример (слегка измененный для ясности):

import six

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

3

Вы можете использовать мой класс CausedException для объединения исключений в Python 2.x (и даже в Python 3 это может быть полезно в случае, если вы хотите указать более одного перехваченного исключения в качестве причины для вновь возникшего исключения). Может быть, это может помочь вам.


2

Может быть, вы могли бы получить соответствующую информацию и передать ее? Я думаю что-то вроде:

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

Предполагая, что:

  • вам нужно решение, которое работает для Python 2 (для чистого Python 3 см. raise ... fromрешение)
  • просто хочу обогатить сообщение об ошибке, например, предоставив некоторый дополнительный контекст
  • нужна полная трассировка стека

Вы можете использовать простое решение из документации https://docs.python.org/3/tutorial/errors.html#raising-exceptions :

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

Выход:

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

Похоже, ключевой элемент - это упрощенное ключевое слово «поднимать», которое стоит отдельно. Это повторно вызовет исключение в блоке исключений.


Это Python 2 & 3 совместимое решение! Спасибо!
Энди Чейз

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

2
Это не цепочка вложенных исключений, просто повторное поднятие одного исключения
Карл Рихтер

Это лучшее решение Python 2, если вам просто нужно обогатить сообщение об исключении и получить полную трассировку стека!
geekQ

В чем разница между использованием рейза и рейза из
переменная
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.