"Внутрішній виняток" (із відстеженням) у 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.