"Внутрішній виняток" (із відстеженням) у Python?


146

Моє передумови в 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, на жаль, ОП не бачились з 2015 року.
Антті

Відповіді:


136

Пітон 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

Пітон 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Я думаю, це тому, що, на жаль, більшість людей досі не користуються Python 3.
Тім Людвинський

Здається, це трапляється навіть при використанні "від" в 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 / ... з clearification беручи до уваги ваш відповідь безпосередньо. Ми повинні там обговорити :)
Карл Ріхтер

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 для розширення того, як вони працюють)

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: підвищення: arg 3 має бути прослідковуванням або None
Гленн Мейнард

Вибачте, я помилився, я чомусь подумав, що він також приймає винятки і автоматично отримує їх атрибут 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." Ця властивість не встановлюється VM автоматично; швидше, всі конструктори винятків беруть необов'язковий аргумент 'innerException', щоб встановити його явно. Атрибут " причина " виконує ту саму мету, що і InnerException, але цей PEP пропонує нову форму "підвищення", а не розширення конструкторів усіх винятків. C # також забезпечує метод GetBaseException, який переходить безпосередньо до кінця ланцюга InnerException;

Зауважте також, що Java, Ruby та Perl 5 теж не підтримують цей тип речей. Цитую ще раз:

Що стосується інших мов, Java та Ruby відкидають оригінальний виняток, коли виникає інший виняток у пункті "catch" / "рятування" або "нарешті" / "забезпечити". У Perl 5 бракує вбудованої структурованої обробки винятків. Для Perl 6 RFC номер 88 [9] пропонує механізм винятку, який неявно зберігає ланцюгові винятки в масиві з назвою @@.


Але, звичайно, в Perl5 ви можете просто сказати "confess 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

Ви можете використовувати мій клас CauseException для ланцюжки винятків у 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.