Ponownie zgłoś wyjątek z innym typem i komunikatem, zachowując istniejące informacje

139

Piszę moduł i chcę mieć ujednoliconą hierarchię wyjątków dla wyjątków, które może on wywołać (np. Dziedziczenie z FooErrorklasy abstrakcyjnej dla wszystkich foowyjątków specyficznych dla modułu). Pozwala to użytkownikom modułu na wychwycenie tych wyjątków i wyraźną obsługę ich w razie potrzeby. Ale wiele wyjątków zgłoszonych z modułu jest zgłaszanych z powodu innego wyjątku; np. niepowodzenie w wykonaniu jakiegoś zadania z powodu błędu OSError w pliku.

To, czego potrzebuję, to „zawinąć” przechwycony wyjątek w taki sposób, aby miał inny typ i inny komunikat , aby informacje były dostępne na wyższych poziomach hierarchii propagacji przez wszystko, co przechwytuje wyjątek. Ale nie chcę stracić istniejącego typu, komunikatu i śladu stosu; to wszystko jest przydatne dla kogoś, kto próbuje debugować problem. Obsługa wyjątków najwyższego poziomu nie jest dobra, ponieważ próbuję udekorować wyjątek, zanim przejdzie dalej w górę stosu propagacji, a program obsługi najwyższego poziomu jest za późno.

Jest to częściowo rozwiązane poprzez wyprowadzenie foospecyficznych typów wyjątków mojego modułu z istniejącego typu (np. class FooPermissionError(OSError, FooError)), Ale to nie ułatwia pakowania istniejącego wystąpienia wyjątku w nowy typ ani modyfikowania komunikatu.

Python PEP 3134 „Exception Chaining and Embedded Tracebacks” omawia zmianę zaakceptowaną w Pythonie 3.0 dla „łańcuchowego” obiektów wyjątków, aby wskazać, że nowy wyjątek został zgłoszony podczas obsługi istniejącego wyjątku.

To, co próbuję zrobić, jest powiązane: potrzebuję tego również we wcześniejszych wersjach Pythona i nie potrzebuję go do tworzenia łańcuchów, a tylko do polimorfizmu. Jaki jest właściwy sposób, aby to zrobić?

duży nos
źródło
Wyjątki są już całkowicie polimorficzne - wszystkie są podklasami wyjątku. Co próbujesz zrobić? „Inna wiadomość” jest dość trywialna dzięki obsłudze wyjątków najwyższego poziomu. Dlaczego zmieniasz klasę?
S.Lott,
Jak wyjaśniono w pytaniu (teraz dziękuję za komentarz): Próbuję udekorować wyjątek, który złapałem, aby mógł się dalej rozprzestrzeniać z większą ilością informacji, ale nie tracąc żadnego. Osoba obsługująca najwyższego poziomu jest za późno.
bignose
Proszę spojrzeć na moją klasę CausedException, która może robić, co chcesz w Pythonie 2.x. Również w Pythonie 3 może się przydać, jeśli chcesz podać więcej niż jeden oryginalny wyjątek jako przyczynę wyjątku. Może pasuje do Twoich potrzeb.
Alfe,
Dla pythona-2 robię coś podobnego do @DevinJeanpierre, ale właśnie dołączam nową wiadomość tekstową: except Exception as e-> raise type(e), type(e)(e.message + custom_message), sys.exc_info()[2]-> to rozwiązanie pochodzi z innego pytania SO . To nie jest ładne, ale funkcjonalne.
Trevor Boyd Smith

Odpowiedzi:

197

Python 3 wprowadził łańcuch wyjątków (zgodnie z opisem w PEP 3134 ). Dzięki temu podczas zgłaszania wyjątku można zacytować istniejący wyjątek jako „przyczynę”:

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

W ten sposób przechwycony wyjątek staje się częścią (jest „przyczyną”) nowego wyjątku i jest dostępny dla każdego kodu przechwytującego nowy wyjątek.

Korzystając z tej funkcji, __cause__ustawiany jest atrybut. Wbudowana procedura obsługi wyjątków wie również, jak zgłosić „przyczynę” i „kontekst” wyjątku wraz ze śledzeniem.


W Pythonie 2 wydaje się, że ten przypadek użycia nie ma dobrej odpowiedzi (jak opisali Ian Bicking i Ned Batchelder ). Porażka.

duży nos
źródło
4
Czy Ian Bicking nie opisuje mojego rozwiązania? Żałuję, że udzieliłem tak okropnej odpowiedzi, ale to dziwne, że ta została przyjęta.
Devin Jeanpierre
1
@bignose Miałeś mój punkt widzenia nie tylko dlatego, że masz rację, ale także używając „frobnicate” :)
David M.,
5
Tworzenie łańcuchów wyjątków jest obecnie domyślnym zachowaniem, w rzeczywistości jest to odwrotność problemu, pomijając pierwszy wyjątek, który wymaga pracy, patrz PEP 409 python.org/dev/peps/pep-0409
Chris_Rands
1
Jak byś tego dokonał w Pythonie 2?
selotape
1
Wydaje się, że działa dobrze (python 2.7)try: return 2 / 0 except ZeroDivisionError as e: raise ValueError(e)
alex
37

Możesz użyć sys.exc_info (), aby uzyskać śledzenie i zgłosić nowy wyjątek za pomocą wspomnianego śledzenia (jak wspomina PEP). Jeśli chcesz zachować stary typ i wiadomość, możesz to zrobić na wyjątku, ale jest to przydatne tylko wtedy, gdy szuka go cokolwiek, co wyłapuje twój wyjątek.

Na przykład

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

Oczywiście to naprawdę nie jest przydatne. Gdyby tak było, nie potrzebowalibyśmy tego PEP. Nie polecałbym tego robić.

Devin Jeanpierre
źródło
Devin, przechowujesz tam odniesienie do śledzenia, czy nie powinieneś jawnie usuwać tego odniesienia?
Arafangion,
2
Nie zapisałem niczego, zostawiłem śledzenie jako zmienną lokalną, która prawdopodobnie wykracza poza zakres. Tak, można sobie wyobrazić, że tak nie jest, ale jeśli podniesiesz takie wyjątki w zakresie globalnym, a nie w ramach funkcji, masz większe problemy. Jeśli twoja skarga dotyczy tylko tego, że można ją wykonać w zakresie globalnym, właściwym rozwiązaniem nie jest dodanie nieistotnego schematu, który musi zostać wyjaśniony i nie dotyczy 99% zastosowań, ale przepisanie rozwiązania tak, aby nic takiego nie było jest konieczne, gdy wydaje się, że nic się nie zmieniło - tak jak teraz.
Devin Jeanpierre
4
Arafangion może odnosić się do ostrzeżenia w dokumentacji Pythona dlasys.exc_info() @Devin. Mówi się, że „przypisanie wartości zwracanej przez dane śledzenia do zmiennej lokalnej w funkcji obsługującej wyjątek spowoduje odwołanie cykliczne”. Jednak następująca uwaga mówi, że od Pythona 2.2 cykl można wyczyścić, ale bardziej efektywne jest po prostu unikanie go.
Don Kirkby,
5
Więcej szczegółów na temat różnych sposobów ponownego zgłaszania wyjątków w Pythonie od dwóch oświeconych pytonistów: Ian Bicking i Ned Batchelder
Rodrigue
11

Możesz utworzyć własny typ wyjątku, który rozszerza każdy wychwycony wyjątek .

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

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

Ale przez większość czasu myślę, że łatwiej byłoby złapać wyjątek, obsłużyć go i albo raiseoryginalny wyjątek (i zachować śledzenie), albo raise NewException(). Gdybym wywoływał twój kod i otrzymałem jeden z twoich niestandardowych wyjątków, spodziewałbym się, że twój kod już obsłużył każdy wyjątek, który musiałeś złapać. Dlatego nie muszę sam uzyskiwać do niego dostępu.

Edycja: znalazłem tę analizę sposobów zgłaszania własnego wyjątku i zachowania oryginalnego wyjątku. Żadnych ładnych rozwiązań.

Nikhil Chelliah
źródło
1
Opisany przeze mnie przypadek użycia nie służy do obsługi wyjątku; to specjalnie o nie posługiwaniu się nim, ale dodając kilka dodatkowych informacji (dodatkową klasę i nową wiadomość) tak, że może być obsługiwany wyżej na stosie wywołań.
bignose
2

Zauważyłem też, że wiele razy potrzebuję „zawijania” podniesionych błędów.

Obejmuje to zarówno zakres funkcji, jak i czasami zawija tylko niektóre linie wewnątrz funkcji.

Utworzono opakowanie do użycia a decoratori context manager:


Realizacja

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

Przykłady użycia

dekorator

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

podczas wywoływania dometody nie martw się IndexError, po prostuMyError

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

menedżer kontekstu

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

wewnątrz do2, context managerjeśli IndexErrorzostanie podniesiony, zostanie zawinięty i podniesionyMyError

Aaron_ab
źródło
1
Proszę wyjaśnić, co „zawijanie” zrobi z oryginalnym wyjątkiem. Jaki jest cel twojego kodu i jakie zachowanie umożliwia?
Alexis
@alexis - dodał kilka przykładów, mam nadzieję, że to pomoże
Aaron_ab
-2

Najbardziej prostym rozwiązaniem dla Twoich potrzeb powinno być:

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

W ten sposób możesz później wydrukować swoją wiadomość i określony błąd wyrzucony przez funkcję wysyłania

funny_dev
źródło
1
To łapie, a następnie odrzuca obiekt wyjątku, więc nie, nie spełnia on potrzeb pytania. Pytanie zawiera pytanie, jak zachować istniejący wyjątek i zezwolić temu samemu wyjątkowi, wraz ze wszystkimi użytecznymi informacjami, które zawiera, na dalsze propagowanie stosu.
bignose