Повторно підняти виняток з іншим типом та повідомленням, зберігши наявну інформацію


139

Я пишу модуль і хочу мати єдину ієрархію винятків за винятками, які він може викликати (наприклад, успадкування від FooErrorабстрактного класу за всіма fooвинятками модуля). Це дозволяє користувачам модуля вловлювати ці конкретні винятки та чітко обробляти їх, якщо потрібно. Але багато винятків, викладених із модуля, виникають через якийсь інший виняток; наприклад, невдача в якомусь завданні через OSError у файлі.

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

Частково це вирішується шляхом отримання fooспецифічних типів виключень мого модуля з існуючого типу (наприклад class FooPermissionError(OSError, FooError)), але це не полегшує перенесення існуючого екземпляра винятків у новий тип, а також не змінення повідомлення.

PEP 3134 Python "Виключення ланцюга та вбудовані відстеження" обговорює зміну, прийняту в Python 3.0 для "ланцюжків" об'єктів винятків, щоб вказати, що під час обробки існуючого винятку було піднято нове виключення.

Те, що я намагаюся зробити, пов'язане: мені це потрібно і в більш ранніх версіях Python, і мені це потрібно не для прикування, а лише для поліморфізму. Який правильний спосіб це зробити?


Винятки вже є повністю поліморфними - вони всі підкласи винятку. Що ти намагаєшся зробити? "Різне повідомлення" є досить тривіальним із обробником винятків вищого рівня. Чому ти міняєш клас?
С.Лотт

Як пояснено у запитанні (зараз, дякую за ваш коментар): Я намагаюся прикрасити виняток, який я потрапив, щоб він міг поширюватись ще більше інформації, але не втрачаючи жодної. Обробник верхнього рівня занадто пізно.
bignose

Будь ласка, подивіться на мій клас CauseException, який може робити все, що вам потрібно в Python 2.x. Також в Python 3 він може бути корисним у випадку, якщо ви хочете дати більше одного оригінального винятку як причину вашого винятку. Можливо, це відповідає вашим потребам.
Альфе


Для python-2 я щось подібне до @DevinJeanpierre, але я просто додаю нове рядкове повідомлення: except Exception as e-> raise type(e), type(e)(e.message + custom_message), sys.exc_info()[2]-> це рішення з іншого питання SO . Це не дуже, але функціонально.
Тревор Бойд Сміт

Відповіді:


197

Python 3 представив ланцюжок виключень (як описано в PEP 3134 ). Це дозволяє, піднімаючи виняток, цитувати існуючий виняток як "причину":

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

Виняте виняток, таким чином, стає частиною (є "причиною") нового винятку і є доступним для будь-якого коду, який сприймає новий виняток.

За допомогою цієї функції встановлюється __cause__атрибут. Вбудований обробник винятків також знає, як повідомити про "причину" та "контекст" виключення разом із простеженням.


У Python 2 , здається, цей випадок використання не має хорошої відповіді (як описано Іен Бікінг та Нед Батчелдер ). Бампер.


4
Хіба Ян Бікінг не описує моє рішення? Я шкодую, що дав таку побожну відповідь, але дивно, що цього прийняли.
Девін Жанп'єр

1
@bignose Ви отримали мою думку не лише від того, що я маю рацію, але і за використання "frobnicate" :)
David M.

5
Сфальсифікація винятків - це фактично поведінка за замовчуванням зараз, насправді це навпаки, придушуючи перше виключення, яке вимагає роботи, див. PEP 409 python.org/dev/peps/pep-0409
Chris_Rands

1
Як би ви це зробили в python 2?
selotape

1
Здається, працює добре (python 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

Звичайно, це насправді не так корисно. Якби це було, нам цей ПЕП не знадобився. Я б не рекомендував це робити.


Девіне, ти зберігаєш там посилання на зворотній зв'язок, чи не слід ви явно видаляти це посилання?
Арафангіон

2
Я нічого не зберігав, я залишив прослідкування як локальну змінну, яка, ймовірно, випадає із сфери застосування. Так, можливо, це не так, але якщо ви ставите подібні винятки в глобальному масштабі, а не в межах функцій, у вас виникають більші проблеми. Якщо Ваша скарга полягає лише в тому, що вона може бути виконана в глобальному масштабі, правильне рішення полягає не в тому, щоб додавати невідповідні котлети, які повинні бути пояснені і не мають значення для 99% використання, а переписати рішення, щоб такого не було. необхідно, роблячи так, ніби нічого не відрізняється-- як я це зробив зараз.
Девін Жанп'єр

4
Arafangion, можливо, посилається на попередження в документації Python дляsys.exc_info() @Devin. У ній сказано: "Присвоєння значення зворотного відстеження локальній змінній функції, яка обробляє виняток, спричинить циркулярну посилання". Однак, наступна примітка говорить про те, що з моменту Python 2.2 цикл можна очистити, але ефективніше просто уникати цього.
Дон Кіркбі

5
Більш детально про різні способи відновити винятки в Python з двох освічених пітоністів: Ian Bicking та Ned Batchelder
Rodrigue

11

Ви можете створити свій власний тип виключення, який поширюється на той виняток, який ви потрапили.

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

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

Але більшу частину часу я вважаю, що було б простіше зловити виняток, обробити його і або raiseвихідний виняток (і зберегти прослідкування), або raise NewException(). Якби я дзвонив вашому коду, і я отримав одне з ваших спеціальних винятків, я би сподівався, що ваш код уже обробляє будь-який виняток, який вам довелося спіймати. Тому мені не потрібно самостійно отримувати доступ до нього.

Редагувати: Я знайшов цей аналіз способів викинути власний виняток і зберегти початковий виняток. Немає гарних рішень.


1
Прецедент я описав не для обробки виключення; мова йде конкретно про те, щоб не обробляти його, а додавати додаткову інформацію (додатковий клас та нове повідомлення), щоб з ним можна було оброблятись далі в стек викликів.
bignose

2

Я також виявив, що мені багато разів потрібна певна обгортка для виниклих помилок.

Це включало як в область функцій, так і іноді загортати лише деякі рядки всередині функції.

Створена обгортка для використання 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


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)

Таким чином ви зможете пізніше надрукувати своє повідомлення та конкретну помилку, яку викинула функція завантаження


1
Це ловить, а потім викидає об'єкт виключення, тому ні, це не відповідає потребам питання. Питання задає питання, як зберегти існуючий виняток і дозволити тому самому винятку з усією корисною інформацією, яку він містить, продовжувати розповсюдження стека.
bignose
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.