„Wewnętrzny wyjątek” (ze śledzeniem) w Pythonie?

146

Moje doświadczenie jest w C # i niedawno zacząłem programować w Pythonie. Gdy wyjątek jest zgłaszany, zwykle chcę zawinąć go w inny wyjątek, który dodaje więcej informacji, jednocześnie pokazując pełny ślad stosu. W C # jest to dość łatwe, ale jak to zrobić w Pythonie?

Na przykład. w C # zrobiłbym coś takiego:

try
{
  ProcessFile(filePath);
}
catch (Exception ex)
{
  throw new ApplicationException("Failed to process file " + filePath, ex);
}

W Pythonie mogę zrobić coś podobnego:

try:
  ProcessFile(filePath)
except Exception as e:
  raise Exception('Failed to process file ' + filePath, e)

... ale to powoduje utratę śledzenia wewnętrznego wyjątku!

Edycja: chciałbym zobaczyć zarówno komunikaty o wyjątkach, jak i oba ślady stosu i skorelować je. Oznacza to, że chcę zobaczyć w danych wyjściowych, że wyjątek X wystąpił tutaj, a następnie wyjątek Y tam - tak samo, jak w C #. Czy jest to możliwe w Pythonie 2.6? Wygląda na to, że najlepsze, co do tej pory mogę zrobić (na podstawie odpowiedzi Glenna Maynarda), to:

try:
  ProcessFile(filePath)
except Exception as e:
  raise Exception('Failed to process file' + filePath, e), None, sys.exc_info()[2]

Obejmuje to zarówno komunikaty, jak i oba elementy śledzenia, ale nie pokazuje, który wyjątek wystąpił w miejscu śledzenia.

EMP
źródło
3
Przyjęta odpowiedź staje się nieaktualna, być może powinieneś rozważyć przyjęcie innej.
Aaron Hall
1
@AaronHall niestety OP nie było widziane od 2015 roku.
Antti Haapala

Odpowiedzi:

136

Python 2

To proste; przekazuje dane śledzenia jako trzeci argument do podniesienia.

import sys
class MyException(Exception): pass

try:
    raise TypeError("test")
except TypeError, e:
    raise MyException(), None, sys.exc_info()[2]

Rób to zawsze podczas wychwytywania jednego wyjątku i ponownego zgłaszania innego.

Glenn Maynard
źródło
4
Dzięki. To zachowuje śledzenie, ale powoduje utratę komunikatu o błędzie oryginalnego wyjątku. Jak wyświetlić zarówno komunikaty, jak i oba dane śledzenia?
EMP
6
raise MyException(str(e)), ...itp.
Glenn Maynard
23
Python 3 dodaje raise E() from tbi.with_traceback(...)
Dima Tisnek
3
@GlennMaynard to dość stare pytanie, ale środkowy argument raisejest wartością przekazywaną do wyjątku (w przypadku, gdy pierwszy argument jest klasą wyjątku, a nie instancją). Więc jeśli chcesz wyjątkami typu swap, zamiast robić raise MyException(str(e)), None, sys.exc_info()[2], to lepiej korzystać z tego: raise MyException, e.args, sys.exc_info()[2].
bgusach
8
Sposób zgodny A python2 i 3, możliwe jest przy użyciu pakietu przyszłość python-future.org/compatible_idioms.html#raising-exceptions Np from future.utils import raise_i raise_(ValueError, None, sys.exc_info()[2]).
jtpereyda
239

Python 3

W Pythonie 3 możesz wykonać następujące czynności:

try:
    raise MyExceptionToBeWrapped("I have twisted my ankle")

except MyExceptionToBeWrapped as e:

    raise MyWrapperException("I'm not in a good shape") from e

To da coś takiego:

   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")
Aleksiej Tenitski
źródło
17
raise ... from ...jest rzeczywiście właściwym sposobem na zrobienie tego w Pythonie 3. To wymaga więcej głosów pozytywnych.
Nakedible
NakedibleMyślę, że to dlatego, że niestety większość ludzi nadal nie używa Pythona 3.
Tim Ludwinski
Wydaje się, że dzieje się tak nawet przy użyciu słowa „from” w Pythonie 3
Steve Vermeulen
Może zostać przeniesiony do Pythona 2. Mam nadzieję, że pewnego dnia tak się stanie.
Marcin Wojnarski
4
@ogrisel Możesz użyć futurepakietu, aby to osiągnąć: python-future.org/compatible_idioms.html#raising-exceptions Np . from future.utils import raise_i raise_(ValueError, None, sys.exc_info()[2]).
jtpereyda
19

Python 3 ma klauzulę raise...from do łączenia wyjątków. Odpowiedź Glenna jest świetna dla Pythona 2.7, ale używa tylko śledzenia oryginalnego wyjątku i odrzuca komunikat o błędzie i inne szczegóły. Oto kilka przykładów w Pythonie 2.7, które dodają informacje o kontekście z bieżącego zakresu do oryginalnego komunikatu o błędzie, ale pozostałe szczegóły pozostają nienaruszone.

Znany typ wyjątku

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

Ten raiserodzaj instrukcji przyjmuje typ wyjątku jako pierwsze wyrażenie, argumenty konstruktora klasy wyjątku w krotce jako drugie wyrażenie, a śledzenie zwrotne jako trzecie wyrażenie. Jeśli używasz wersji wcześniejszej niż Python 2.2, zobacz ostrzeżenia nasys.exc_info() .

Dowolny typ wyjątku

Oto kolejny przykład, który jest bardziej ogólny, jeśli nie wiesz, jakie wyjątki może przechwytywać Twój kod. Wadą jest to, że traci typ wyjątku i po prostu zgłasza błąd RuntimeError. Musisz zaimportować tracebackmoduł.

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

Zmodyfikuj wiadomość

Oto inna opcja, jeśli typ wyjątku pozwoli ci dodać do niego kontekst. Możesz zmodyfikować komunikat wyjątku, a następnie ponownie go podnieść.

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

To generuje następujący ślad stosu:

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']

Możesz zobaczyć, że pokazuje on wiersz, w którym check_output()został wywołany, ale komunikat o wyjątku zawiera teraz wiersz poleceń.

Don Kirkby
źródło
1
Skąd ex.strerrorpochodzi? Nie mogę znaleźć żadnego odpowiedniego trafienia w dokumentacji Pythona. Nie powinno być str(ex)?
Henrik Heimbuerger,
1
IOErrorpochodzi z EnvironmentError@hheimbuerger, który udostępnia atrybuty errornoi strerror.
Don Kirkby,
Jak zawinąć dowolny Error, np. ValueError, w RuntimeErrorprzechwytując Exception? Jeśli powtórzę twoją odpowiedź w tej sprawie, ślad stosu zostanie utracony.
Karl Richter
Nie jestem pewien, o co pytasz, @karl. Czy możesz opublikować próbkę w nowym pytaniu, a następnie utworzyć łącze do niego z tego miejsca?
Don Kirkby
Edytowałem mój duplikat pytania OP na stackoverflow.com/questions/23157766/… z wyjaśnieniem uwzględniającym bezpośrednio Twoją odpowiedź. Powinniśmy tam omówić :)
Karl Richter
12

W Pythonie 3.x :

raise Exception('Failed to process file ' + filePath).with_traceback(e.__traceback__)

lub po prostu

except Exception:
    raise MyException()

który będzie się propagował, MyExceptionale wypisze oba wyjątki, jeśli nie będzie obsługiwany.

W Pythonie 2.x :

raise Exception, 'Failed to process file ' + filePath, e

Możesz zapobiec drukowaniu obu wyjątków, zabijając __context__atrybut. Tutaj piszę menedżera kontekstu, który używa go do przechwytywania i zmiany wyjątku w locie: (zobacz http://docs.python.org/3.1/library/stdtypes.html, aby dowiedzieć się, jak działają)

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
ilya n.
źródło
4
TypeError: raise: arg 3 musi być traceback lub None
Glenn Maynard
Przepraszam, popełniłem błąd, jakoś pomyślałem, że akceptuje również wyjątki i automatycznie pobiera ich atrybut traceback. Zgodnie z docs.python.org/3.1/reference/… , powinno to być e .__
traceback__
1
@ilyan .: Python 2 nie ma e.__traceback__atrybutu!
Jan Hudec
5

Nie sądzę, że możesz to zrobić w Pythonie 2.x, ale coś podobnego do tej funkcji jest częścią Pythona 3. Z PEP 3134 :

W dzisiejszej implementacji Pythona wyjątki składają się z trzech części: typu, wartości i śledzenia wstecznego. Moduł `` sys '' ujawnia bieżący wyjątek w trzech równoległych zmiennych, exc_type, exc_value i exc_traceback, funkcja sys.exc_info () zwraca krotkę z tych trzech części, a instrukcja `` raise '' ma postać trzyargumentową akceptującą te trzy części. Manipulowanie wyjątkami często wymaga równoległego przekazywania tych trzech rzeczy, co może być żmudne i podatne na błędy. Ponadto instrukcja „oprócz” zapewnia dostęp tylko do wartości, a nie do śledzenia wstecznego. Dodanie atrybutu „ traceback ” do wartości wyjątków sprawia, że ​​wszystkie informacje o wyjątkach są dostępne z jednego miejsca.

Porównanie do C #:

Wyjątki w C # zawierają właściwość „InnerException” tylko do odczytu, która może wskazywać na inny wyjątek. Jego dokumentacja [10] mówi, że „Gdy wyjątek X jest zgłaszany jako bezpośredni skutek poprzedniego wyjątku Y, właściwość InnerException X powinna zawierać odniesienie do Y”. Ta właściwość nie jest ustawiana automatycznie przez maszynę wirtualną; raczej wszystkie konstruktory wyjątków pobierają opcjonalny argument „innerException”, aby ustawić go jawnie. Atrybut „ przyczyna ” spełnia ten sam cel co InnerException, ale ten PEP proponuje nową formę „podniesienia” zamiast rozszerzania konstruktorów wszystkich wyjątków. C # udostępnia również metodę GetBaseException, która przeskakuje bezpośrednio na koniec łańcucha InnerException;

Zauważ również, że Java, Ruby i Perl 5 również nie obsługują tego typu rzeczy. Cytując ponownie:

Podobnie jak w przypadku innych języków, zarówno Java, jak i Ruby odrzucają oryginalny wyjątek, gdy wystąpi inny wyjątek w klauzuli „catch” / „rescue” lub „w końcu” / „zapewnia”. Perl 5 nie ma wbudowanej strukturalnej obsługi wyjątków. W przypadku Perla 6 dokument RFC numer 88 [9] proponuje mechanizm wyjątków, który niejawnie zachowuje łańcuchowe wyjątki w tablicy o nazwie @@.

ire_and_curses
źródło
Ale oczywiście w Perl5 możesz po prostu powiedzieć „confess qq {OH NOES! $ @}” I nie stracić śladu stosu drugiego wyjątku. Lub możesz zaimplementować własny typ, który zachowuje wyjątek.
jrockway
4

Aby uzyskać maksymalną zgodność między Pythonem 2 i 3, możesz użyć raise_fromw sixbibliotece. https://six.readthedocs.io/#six.raise_from . Oto Twój przykład (nieco zmodyfikowany dla przejrzystości):

import six

try:
  ProcessFile(filePath)
except Exception as e:
  six.raise_from(IOError('Failed to process file ' + repr(filePath)), e)
LexH
źródło
3

Możesz użyć mojej klasy CausedException do tworzenia łańcuchów wyjątków w Pythonie 2.x (a nawet w Pythonie 3 może to być przydatne w przypadku, gdy chcesz podać więcej niż jeden przechwycony wyjątek jako przyczynę nowo podniesionego wyjątku). Może to ci pomoże.

Alfe
źródło
2

Może mógłbyś pobrać odpowiednie informacje i przekazać je dalej? Myślę o czymś takim:

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
brool
źródło
2

Zarozumiały:

  • potrzebujesz rozwiązania, które działa dla Pythona 2 (dla czystego Pythona 3 zobacz raise ... from rozwiązanie)
  • po prostu chcesz wzbogacić komunikat o błędzie, np. podając dodatkowy kontekst
  • potrzebny jest pełny ślad stosu

możesz użyć prostego rozwiązania z dokumentacji 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

Wyjście:

An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in ?
NameError: HiThere

Wygląda na to, że kluczowym elementem jest uproszczone słowo kluczowe „podwyżka”, które występuje samodzielnie. Spowoduje to ponowne podniesienie wyjątku w bloku except.

atreat
źródło
To jest rozwiązanie zgodne z Python 2 i 3! Dzięki!
Andy Chase
Myślę, że chodziło o zgłoszenie innego rodzaju wyjątku.
Tim Ludwinski
2
To nie jest łańcuch zagnieżdżonych wyjątków, po prostu wznowienie jednego wyjątku
Karl Richter
To najlepsze rozwiązanie dla Pythona 2, jeśli potrzebujesz tylko wzbogacić komunikat o wyjątku i mieć pełny ślad stosu!
geekQ
Jaka jest różnica między używaniem podbicia a podbicia z
zmienna