„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.


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.


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")

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ń.


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

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 @@.


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)

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.


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

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.


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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.