Повторно вызвать исключение с другим типом и сообщением, сохраняя существующую информацию

145

Я пишу модуль и хочу иметь единую иерархию исключений для исключений, которые он может вызывать (например, наследование от FooErrorабстрактного класса для всех fooособых исключений модуля). Это позволяет пользователям модуля перехватывать эти конкретные исключения и при необходимости обрабатывать их отдельно. Но многие исключения, вызванные модулем, возникают из-за другого исключения; например, сбой при выполнении какой-либо задачи из-за ошибки OSError в файле.

Мне нужно «обернуть» перехваченное исключение таким образом, чтобы оно имело другой тип и сообщение , чтобы информация была доступна дальше по иерархии распространения тем, что перехватывает исключение. Но я не хочу терять существующий тип, сообщение и трассировку стека; это вся полезная информация для тех, кто пытается устранить проблему. Обработчик исключений верхнего уровня бесполезен, поскольку я пытаюсь украсить исключение до того, как оно продвинется дальше по стеку распространения, а обработчик верхнего уровня опаздывает.

Это частично решается путем получения fooконкретных типов исключений моего модуля из существующего типа (например, class FooPermissionError(OSError, FooError)), но это не упрощает перенос существующего экземпляра исключения в новый тип или изменение сообщения.

В Python PEP 3134 «Цепочка исключений и встроенная трассировка» обсуждается изменение, принятое в Python 3.0 для «цепочки» объектов исключений, чтобы указать, что новое исключение было вызвано во время обработки существующего исключения.

То, что я пытаюсь сделать, связано: мне нужно, чтобы он работал и в более ранних версиях Python, и мне он нужен не для связывания, а только для полиморфизма. Как правильно это сделать?

большой нос
источник
Исключения уже полностью полиморфны - все они подклассы Exception. Что ты пытаешься сделать? «Другое сообщение» довольно тривиально для обработчика исключений верхнего уровня. Почему вы меняете класс?
S.Lott
Как объясняется в вопросе (теперь спасибо за ваш комментарий): я пытаюсь украсить исключение, которое я поймал, чтобы оно могло распространяться дальше с дополнительной информацией, но не теряя ее. Обработчик верхнего уровня опаздывает.
bignose
Взгляните на мой класс CausedException, который может делать то, что вы хотите, в Python 2.x. Также в Python 3 он может быть полезен, если вы хотите указать более одного исходного исключения в качестве причины вашего исключения. Может быть, это соответствует вашим потребностям.
Alfe 05
Для python-2 я делаю что-то похожее на @DevinJeanpierre, но я просто добавляю новое строковое сообщение: except Exception as e-> raise type(e), type(e)(e.message + custom_message), sys.exc_info()[2]-> это решение из другого вопроса SO . Это некрасиво, но функционально.
Trevor Boyd Smith

Ответы:

209

Python 3 представил цепочку исключений (как описано в PEP 3134 ). Это позволяет при возникновении исключения ссылаться на существующее исключение как на «причину»:

try:
    frobnicate()
except KeyError as exc:
    raise ValueError("Bad grape") from exc

excПойманное исключение ( , KeyError) таким образом становится частью (является «причиной») нового исключения - ValueError. «Причина» доступна для любого кода, улавливающего новое исключение.

При использовании этой функции __cause__атрибут устанавливается. Встроенный обработчик исключений также знает, как сообщить о «причине» и «контексте» исключения вместе с трассировкой.


В Python 2 , похоже, этот вариант использования не имеет хорошего ответа (как описано Яном Бикингом и Недом Батчелдером ). Облом.

большой нос
источник
4
Разве Ян Бикинг не описывает мое решение? Я сожалею, что дал такой ужасный ответ, но странно, что этот был принят.
Девин Жанпьер
1
@bignose Ты понял мою точку зрения не только из-за того, что был прав, но и из-за использования слова "frobnicate" :)
Дэвид М.
5
Цепочка исключений на самом деле является поведением по умолчанию сейчас, на самом деле это противоположная проблема, подавление первого исключения, которое требует работы, см. PEP 409 python.org/dev/peps/pep-0409
Chris_Rands
1
Как бы вы это сделали в Python 2?
Видеозапись
1
Это , кажется, работает нормально (питон 2,7)try: return 2 / 0 except ZeroDivisionError as e: raise ValueError(e)
Алекс
37

Вы можете использовать sys.exc_info () для получения трассировки и вызвать новое исключение с помощью указанной трассировки (как упоминается в PEP). Если вы хотите сохранить старый тип и сообщение, вы можете сделать это для исключения, но это полезно, только если то, что улавливает ваше исключение, ищет его.

Например

import sys

def failure():
    try: 1/0
    except ZeroDivisionError, e:
        type, value, traceback = sys.exc_info()
        raise ValueError, ("You did something wrong!", type, value), traceback

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

Девин Жанпьер
источник
Девин, вы храните здесь ссылку на трассировку, не следует ли вам явно удалить эту ссылку?
Арафангион,
2
Я ничего не сохранял, я оставил трассировку как локальную переменную, которая предположительно выпадает из области видимости. Да, вполне возможно, что это не так, но если вы вызовете подобные исключения в глобальной области, а не внутри функций, у вас возникнут более серьезные проблемы. Если ваша жалоба заключается только в том, что его можно выполнить в глобальном масштабе, правильным решением будет не добавление ненужного шаблона, который нужно объяснять и не актуален для 99% случаев использования, а переписать решение так, чтобы ничего подобного не было. необходимо, при этом создавая впечатление, будто ничего не изменилось - как я это сделал сейчас.
Девин Жанпьер,
4
Арафангион может иметь в виду предупреждение в документации Python дляsys.exc_info() @Devin. В нем говорится: «Назначение возвращаемого значения трассировки локальной переменной в функции, которая обрабатывает исключение, вызовет циклическую ссылку». Однако в следующем примечании говорится, что, начиная с Python 2.2, цикл можно очистить, но более эффективно просто избегать этого.
Дон Киркби
5
Подробнее о различных способах повторного вызова исключений в Python от двух просвещенных питонистов: Яна Бикинга и Неда Батчелдера
Родриг
11

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

class NewException(CaughtException):
    def __init__(self, caught):
        self.caught = caught

try:
    ...
except CaughtException as e:
    ...
    raise NewException(e)

Но в большинстве случаев я думаю, что было бы проще поймать исключение, обработать его и либо raiseисходное исключение (и сохранить трассировку), либо raise NewException(). Если бы я вызвал ваш код и получил одно из ваших пользовательских исключений, я бы ожидал, что ваш код уже обработал любое исключение, которое вам нужно было перехватить. Таким образом, мне не нужно обращаться к нему самому.

Изменить: я нашел этот анализ способов выбросить собственное исключение и сохранить исходное исключение. Нет хороших решений.

Никхил Челлия
источник
1
Описанный мной вариант использования не предназначен для обработки исключения; это конкретно о не обрабатывать его, а добавить некоторую дополнительную информацию (дополнительный класс и новое сообщение), чтобы его можно было обрабатывать дальше по стеку вызовов.
bignose
3

Я также обнаружил, что много раз мне нужно «обернуть» возникшие ошибки.

Это входит как в область видимости функции, так и иногда включает только некоторые строки внутри функции.

Создана оболочка для использования decoratorи context manager:


Реализация

import inspect
from contextlib import contextmanager, ContextDecorator
import functools    

class wrap_exceptions(ContextDecorator):
    def __init__(self, wrapper_exc, *wrapped_exc):
        self.wrapper_exc = wrapper_exc
        self.wrapped_exc = wrapped_exc

    def __enter__(self):
        pass

    def __exit__(self, exc_type, exc_val, exc_tb):
        if not exc_type:
            return
        try:
            raise exc_val
        except self.wrapped_exc:
            raise self.wrapper_exc from exc_val

    def __gen_wrapper(self, f, *args, **kwargs):
        with self:
            for res in f(*args, **kwargs):
                yield res

    def __call__(self, f):
        @functools.wraps(f)
        def wrapper(*args, **kw):
            with self:
                if inspect.isgeneratorfunction(f):
                    return self.__gen_wrapper(f, *args, **kw)
                else:
                    return f(*args, **kw)
        return wrapper

Примеры использования

декоратор

@wrap_exceptions(MyError, IndexError)
def do():
   pass

при звонке do метода не беспокойтесь IndexError, простоMyError

try:
   do()
except MyError as my_err:
   pass # handle error 

менеджер контекста

def do2():
   print('do2')
   with wrap_exceptions(MyError, IndexError):
       do()

внутри do2, в context manager, если IndexErrorподнят, он будет завернут и поднятMyError

Aaron_ab
источник
1
Пожалуйста, объясните, что будет делать "упаковка" с исходным исключением. Какова цель вашего кода и какое поведение он обеспечивает?
Alexis
@alexis - добавил несколько примеров, надеюсь, поможет
Aaron_ab
-2

Самым простым решением ваших задач должно быть следующее:

try:
     upload(file_id)
except Exception as upload_error:
     error_msg = "Your upload failed! File: " + file_id
     raise RuntimeError(error_msg, upload_error)

Таким образом, вы можете позже распечатать свое сообщение и конкретную ошибку, выданную функцией загрузки.

funny_dev
источник
1
Это улавливает, а затем отбрасывает объект исключения, поэтому нет, это не соответствует потребностям вопроса. Вопрос спрашивает, как сохранить существующее исключение и разрешить тому же исключению со всей полезной информацией, которую оно содержит, продолжать распространение вверх по стеку.
bignose