Właściwy sposób deklarowania niestandardowych wyjątków we współczesnym języku Python?

1288

Jaki jest właściwy sposób deklarowania niestandardowych klas wyjątków we współczesnym Pythonie? Moim głównym celem jest podążanie za wszystkimi standardowymi innymi klasami wyjątków, aby (na przykład) każdy dodatkowy ciąg, który uwzględniam w wyjątku, był drukowany przez dowolne narzędzie, które wychwyciło wyjątek.

Przez „nowoczesny Python” rozumiem coś, co będzie działało w Pythonie 2.5, ale będzie „poprawne” dla Pythona 2.6 i Pythona 3. * sposób robienia rzeczy. I przez „niestandardowy” rozumiem obiekt wyjątku, który może zawierać dodatkowe dane o przyczynie błędu: ciąg znaków, może również jakiś inny dowolny obiekt związany z wyjątkiem.

Byłem zaskoczony przez następujące ostrzeżenie o wycofaniu w Pythonie 2.6.2:

>>> class MyError(Exception):
...     def __init__(self, message):
...         self.message = message
... 
>>> MyError("foo")
_sandbox.py:3: DeprecationWarning: BaseException.message has been deprecated as of Python 2.6

To szalone, że BaseExceptionma specjalne znaczenie dla nazwanych atrybutów message. Wiem, że z PEP-352 ten atrybut miał specjalne znaczenie w 2.5, które starają się wycofać, więc myślę, że ta nazwa (i ta sama) jest teraz zabroniona? Ugh.

Jestem też dziwnie świadomy tego, że Exceptionma jakiś magiczny parametr args, ale nigdy nie wiedziałem, jak go używać. Nie jestem też pewien, czy jest to właściwy sposób na robienie postępów; wiele dyskusji, które znalazłem w Internecie, sugerowało, że próbowali pozbyć się argumentów w Pythonie 3.

Aktualizacja: dwie odpowiedzi sugerują zastąpienie __init__i __str__/ __unicode__/ __repr__. To chyba dużo pisania, czy to konieczne?

Nelson
źródło

Odpowiedzi:

1322

Może przegapiłem pytanie, ale dlaczego nie:

class MyException(Exception):
    pass

Edycja: aby zastąpić coś (lub przekazać dodatkowe argumenty), wykonaj następujące czynności:

class ValidationError(Exception):
    def __init__(self, message, errors):

        # Call the base class constructor with the parameters it needs
        super(ValidationError, self).__init__(message)

        # Now for your custom code...
        self.errors = errors

W ten sposób możesz przekazać dyktę komunikatów o błędach do drugiego parametru i przejść do tego później e.errors


Aktualizacja Python 3: W Python 3+ możesz użyć tego nieco bardziej kompaktowego zastosowania super():

class ValidationError(Exception):
    def __init__(self, message, errors):

        # Call the base class constructor with the parameters it needs
        super().__init__(message)

        # Now for your custom code...
        self.errors = errors
gahooa
źródło
35
Jednak zdefiniowany w ten sposób wyjątek nie byłby możliwy do przyjęcia; zobacz dyskusję tutaj stackoverflow.com/questions/16244923/…
jiakai
86
@jiakai oznacza „do pobrania”. :-)
Robino,
1
Zgodnie z dokumentacją Pythona dla wyjątków zdefiniowanych przez użytkownika, nazwy wymienione w funkcji __init__ są niepoprawne. Zamiast (ja, komunikat, błąd) jest (ja, wyrażenie, komunikat). Wyrażenie atrybutu jest wyrażeniem wejściowym, w którym wystąpił błąd, a komunikat jest wyjaśnieniem błędu.
ddleon
2
To nieporozumienie, @ddleon. Przykład w dokumentach, do których się odwołujesz, dotyczy konkretnego przypadku użycia. Nazwa argumentów konstruktora podklasy (ani ich liczba) nie ma znaczenia.
asthasr
498

Wyjątki z nowoczesnych Python, nie trzeba do nadużyć .messagelub ręcznym .__str__()lub .__repr__()lub którykolwiek z nim. Jeśli wszystko, czego potrzebujesz, to komunikat informujący o zgłoszeniu wyjątku, wykonaj następujące czynności:

class MyException(Exception):
    pass

raise MyException("My hovercraft is full of eels")

To da nam ślad kończący się na MyException: My hovercraft is full of eels.

Jeśli chcesz uzyskać większą elastyczność od wyjątku, możesz przekazać słownik jako argument:

raise MyException({"message":"My hovercraft is full of animals", "animal":"eels"})

Jednak uzyskanie tych szczegółów w exceptbloku jest nieco bardziej skomplikowane. Szczegóły są przechowywane w argsatrybucie, którym jest lista. Musisz zrobić coś takiego:

try:
    raise MyException({"message":"My hovercraft is full of animals", "animal":"eels"})
except MyException as e:
    details = e.args[0]
    print(details["animal"])

Nadal możliwe jest przekazywanie wielu elementów do wyjątku i uzyskiwanie do nich dostępu za pośrednictwem indeksów krotek, ale jest to bardzo odradzane (a nawet kiedyś miało być wycofane). Jeśli potrzebujesz więcej niż jednej informacji, a powyższa metoda nie jest dla Ciebie wystarczająca, powinieneś podklasę Exceptionzgodnie z opisem w samouczku .

class MyError(Exception):
    def __init__(self, message, animal):
        self.message = message
        self.animal = animal
    def __str__(self):
        return self.message
frnknstn
źródło
2
„ale to będzie przestarzałe w przyszłości” - czy to nadal jest przeznaczone do wycofania? Python 3.7 nadal wydaje się chętnie akceptować Exception(foo, bar, qux).
mtraceur
Nie widziałem żadnych ostatnich prac mających na celu osłabienie go, ponieważ ostatnia próba nie powiodła się z powodu bólu związanego z przejściem, ale takie użycie jest nadal odradzane. Zaktualizuję moją odpowiedź, aby to odzwierciedlić.
frnknstn
@frnknstn, dlaczego jest to odradzane? Wygląda mi na fajny idiom.
neves
2
@neves na początek, używanie krotek do przechowywania informacji o wyjątkach nie ma żadnej przewagi nad używaniem słownika do robienia tego samego. Jeśli jesteś zainteresowany uzasadnieniem zmian wyjątków, spójrz na PEP352
frnknstn
Odpowiednią sekcją PEP352 jest Wycofane pomysły” .
liberforce
196

„Właściwy sposób deklarowania niestandardowych wyjątków we współczesnym Pythonie?”

Jest to w porządku, chyba że twój wyjątek jest naprawdę bardziej szczegółowym wyjątkiem:

class MyException(Exception):
    pass

Lub lepiej (może idealnie), zamiast passpodawać dokumenty:

class MyException(Exception):
    """Raise for my specific kind of exception"""

Podklasy Wyjątek Podklasy

Z dokumentów

Exception

Wszystkie wbudowane wyjątki nie będące wyjściem z systemu pochodzą z tej klasy. Wszystkie wyjątki zdefiniowane przez użytkownika powinny również pochodzić z tej klasy.

Oznacza to, że jeśli twój wyjątek jest rodzajem bardziej szczegółowego wyjątku, podklasuj ten wyjątek zamiast ogólnego Exception(a wynik będzie taki, że nadal wywodzisz się z niego, Exceptionjak zalecają dokumenty). Możesz także podać dokument (i nie zmuszać Cię do używania passsłowa kluczowego):

class MyAppValueError(ValueError):
    '''Raise when my specific value is wrong'''

Ustaw atrybuty, które sam tworzysz za pomocą niestandardowego __init__. Unikaj przekazywania dyktusa jako argumentu pozycyjnego, przyszli użytkownicy Twojego kodu będą Ci wdzięczni. Jeśli użyjesz przestarzałego atrybutu wiadomości, samodzielne przypisanie go pozwoli uniknąć DeprecationWarning:

class MyAppValueError(ValueError):
    '''Raise when a specific subset of values in context of app is wrong'''
    def __init__(self, message, foo, *args):
        self.message = message # without this you may get DeprecationWarning
        # Special attribute you desire with your Error, 
        # perhaps the value that caused the error?:
        self.foo = foo         
        # allow users initialize misc. arguments as any other builtin Error
        super(MyAppValueError, self).__init__(message, foo, *args) 

Naprawdę nie trzeba pisać własnego __str__lub __repr__. Wbudowane są bardzo ładne, a twoje wspólne dziedzictwo zapewnia, że ​​z niego korzystasz.

Krytyka najwyższej odpowiedzi

Może przegapiłem pytanie, ale dlaczego nie:

class MyException(Exception):
    pass

Ponownie, problem z powyższym polega na tym, że aby go złapać, musisz albo nazwać go konkretnie (zaimportować, jeśli został utworzony gdzie indziej), albo złapać wyjątek (ale prawdopodobnie nie jesteś przygotowany do obsługi wszystkich rodzajów wyjątków, i powinieneś wychwytywać tylko wyjątki, na które jesteś przygotowany). Podobna krytyka do poniższej, ale dodatkowo nie jest to sposób inicjowania za pośrednictwem super, a otrzymasz DeprecationWarningdostęp, jeśli uzyskasz dostęp do atrybutu wiadomości:

Edycja: aby zastąpić coś (lub przekazać dodatkowe argumenty), wykonaj następujące czynności:

class ValidationError(Exception):
    def __init__(self, message, errors):

        # Call the base class constructor with the parameters it needs
        super(ValidationError, self).__init__(message)

        # Now for your custom code...
        self.errors = errors

W ten sposób możesz przekazać dyktę komunikatów o błędach do drugiego parametru i przejść do niego później z e.errors

Wymaga również przekazania dokładnie dwóch argumentów (oprócz self.) Nie więcej, nie mniej. To interesujące ograniczenie, którego przyszli użytkownicy mogą nie docenić.

Mówiąc wprost - narusza to substytucyjność Liskova .

Zademonstruję oba błędy:

>>> ValidationError('foo', 'bar', 'baz').message

Traceback (most recent call last):
  File "<pyshell#10>", line 1, in <module>
    ValidationError('foo', 'bar', 'baz').message
TypeError: __init__() takes exactly 3 arguments (4 given)

>>> ValidationError('foo', 'bar').message
__main__:1: DeprecationWarning: BaseException.message has been deprecated as of Python 2.6
'foo'

W porównaniu do:

>>> MyAppValueError('foo', 'FOO', 'bar').message
'foo'
Aaron Hall
źródło
2
Witam z 2018 roku! BaseException.messagezniknął w Pythonie 3, więc krytyka dotyczy tylko starych wersji, prawda?
Kos
5
@Kostyka dotycząca zastępowalności Liskowa jest nadal aktualna. Semantyka pierwszego argumentu jako „wiadomości” również jest wątpliwa, ale nie sądzę, że będę się o to kłócił. Spojrzę na to, kiedy będę mieć więcej wolnego czasu.
Aaron Hall
1
FWIW, w przypadku Pythona 3 (przynajmniej dla wersji 3.6+) należałoby na nowo zdefiniować __str__metodę MyAppValueErrorzamiast polegać na messageatrybucie
Jacquot
1
@AaronHall Czy mógłbyś rozwinąć korzyści wynikające z podklasowania ValueError zamiast wyjątku? Oświadczasz, że to właśnie rozumieją dokumenty, ale bezpośrednie czytanie nie obsługuje tej interpretacji, aw samouczku Python w części Wyjątki zdefiniowane przez użytkownika wyraźnie zaznacza, że ​​to wybór użytkownika: „Wyjątki powinny zazwyczaj pochodzić z klasy wyjątków, bezpośrednio lub pośrednio ”. Dlatego chętnie rozumiem, czy twój pogląd jest uzasadniony.
ostergaard
1
@ostergaard Nie może teraz odpowiedzieć w pełni, ale w skrócie użytkownik otrzymuje dodatkową opcję łapania ValueError. Ma to sens, jeśli należy do kategorii błędów wartości. Jeśli nie jest to kategoria błędów wartości, sprzeciwiłbym się temu w semantyce. Jest miejsce na pewne niuanse i rozumowanie ze strony programisty, ale zdecydowanie wolę specyficzność, jeśli dotyczy. Niedługo zaktualizuję swoją odpowiedź, aby lepiej zająć się tym tematem.
Aaron Hall
50

zobacz, jak domyślnie działają wyjątki, jeśli użyje się jednego atrybutu lub większej liczby (pominięto śledzenie):

>>> raise Exception('bad thing happened')
Exception: bad thing happened

>>> raise Exception('bad thing happened', 'code is broken')
Exception: ('bad thing happened', 'code is broken')

więc możesz chcieć mieć coś w rodzaju „ szablonu wyjątku ”, działającego jako sam wyjątek, w zgodny sposób:

>>> nastyerr = NastyError('bad thing happened')
>>> raise nastyerr
NastyError: bad thing happened

>>> raise nastyerr()
NastyError: bad thing happened

>>> raise nastyerr('code is broken')
NastyError: ('bad thing happened', 'code is broken')

można to łatwo zrobić za pomocą tej podklasy

class ExceptionTemplate(Exception):
    def __call__(self, *args):
        return self.__class__(*(self.args + args))
# ...
class NastyError(ExceptionTemplate): pass

a jeśli nie podoba ci się ta domyślna reprezentacja krotkowa, po prostu dodaj __str__metodę do ExceptionTemplateklasy, na przykład:

    # ...
    def __str__(self):
        return ': '.join(self.args)

i będziesz miał

>>> raise nastyerr('code is broken')
NastyError: bad thing happened: code is broken
mykhal
źródło
32

Począwszy od Python 3.8 (2018, https://docs.python.org/dev/whatsnew/3.8.html ) zalecaną metodą jest nadal:

class CustomExceptionName(Exception):
    """Exception raised when very uncommon things happen"""
    pass

Nie zapomnij udokumentować, dlaczego wyjątek niestandardowy jest konieczny!

Jeśli potrzebujesz, możesz przejść do wyjątków z większą ilością danych:

class CustomExceptionName(Exception):
    """Still an exception raised when uncommon things happen"""
    def __init__(self, message, payload=None):
        self.message = message
        self.payload = payload # you could add more args
    def __str__(self):
        return str(self.message) # __str__() obviously expects a string to be returned, so make sure not to send any other data types

i pobierz je jak:

try:
    raise CustomExceptionName("Very bad mistake.", "Forgot upgrading from Python 1")
except CustomExceptionName as error:
    print(str(error)) # Very bad mistake
    print("Detail: {}".format(error.payload)) # Detail: Forgot upgrading from Python 1

payload=Noneważne jest, aby był zdolny do marynowania. Zanim go wyrzucisz, musisz zadzwonić error.__reduce__(). Ładowanie będzie działać zgodnie z oczekiwaniami.

Być może powinieneś zbadać znalezienie rozwiązania przy użyciu returninstrukcji pytonów , jeśli potrzebujesz dużo danych, aby przenieść je do jakiejś zewnętrznej struktury. Wydaje mi się to wyraźniejsze / bardziej pytoniczne. Zaawansowane wyjątki są często używane w Javie, co czasem może być denerwujące, gdy używa się frameworka i musi wychwycić wszystkie możliwe błędy.

fameman
źródło
1
Przynajmniej obecne dokumenty wskazują, że jest to sposób na zrobienie tego (przynajmniej bez __str__), a nie na inne odpowiedzi, które używają super().__init__(...). Po prostu wstyd, który zastępuje __str__i __repr__prawdopodobnie jest potrzebny tylko dla lepszego „domyślnego” serializacji.
kevlarr
2
Szczere pytanie: dlaczego wyjątki są ważne, aby można je było marynować? Jakie są przypadki zastosowania wyjątków dotyczących zrzutu i ładowania?
Roel Schroeven,
1
@RoelSchroeven: Musiałem raz zrównoleglić kod. Przebiegł dobrze pojedynczy proces, ale niektóre z jego klas nie były serializowane (funkcja lambda przekazywana jako obiekty). Zajęło mi trochę czasu, żeby to rozgryźć i naprawić. To znaczy, że ktoś może później potrzebować serializacji kodu, nie będzie w stanie tego zrobić i będzie musiał wykopać, dlaczego ... Mój problem nie był niemożliwy do usunięcia, ale widzę, że powoduje podobne problemy.
logicOnAbstractions
17

Powinieneś zastąpić __repr__lub __unicode__metody zamiast używać wiadomości, argumenty podane podczas konstruowania wyjątku będą znajdować się w argsatrybucie obiektu wyjątku.

M. Utku ALTINKAYA
źródło
7

Nie, „wiadomość” nie jest zabroniona. To jest po prostu przestarzałe. Twoja aplikacja będzie działać poprawnie przy użyciu wiadomości. Ale oczywiście możesz pozbyć się błędu amortyzacji.

Podczas tworzenia niestandardowych klas wyjątków dla aplikacji wiele z nich nie jest podklasą tylko z wyjątku, ale z innych, takich jak ValueError lub podobny. Następnie musisz dostosować się do ich wykorzystania zmiennych.

A jeśli masz wiele wyjątków w swojej aplikacji, zwykle dobrym pomysłem jest utworzenie wspólnej niestandardowej klasy bazowej dla wszystkich z nich, aby użytkownicy modułów mogli to zrobić

try:
    ...
except NelsonsExceptions:
    ...

I w takim przypadku możesz tam zrobić __init__ and __str__potrzebne, więc nie musisz powtarzać tego dla każdego wyjątku. Ale wystarczy wywołać zmienną message coś innego niż wiadomość.

W każdym razie potrzebujesz tylko, __init__ or __str__jeśli zrobisz coś innego niż to, co robi sam wyjątek. A ponieważ w przypadku wycofania potrzebujesz obu tych elementów lub otrzymasz błąd. To nie jest dużo dodatkowego kodu, którego potrzebujesz na klasę. ;)

Lennart Regebro
źródło
Interesujące jest to, że wyjątki Django nie dziedziczą po wspólnej bazie. docs.djangoproject.com/en/2.2/_modules/django/core/exceptions Czy masz dobry przykład, kiedy potrzebne są wszystkie wyjątki od konkretnej aplikacji? (być może jest to przydatne tylko dla niektórych określonych typów aplikacji).
Yaroslav Nikitenko
Znalazłem dobry artykuł na ten temat, julien.danjou.info/python-exceptions-guide . Myślę, że wyjątki powinny być podklasowane przede wszystkim na podstawie domeny, a nie aplikacji. Gdy twoja aplikacja dotyczy protokołu HTTP, wywodzisz się z HTTPError. Gdy częścią aplikacji jest TCP, wywodzisz wyjątki od tej części z TCPError. Ale jeśli twoja aplikacja obejmuje wiele domen (plik, uprawnienia itp.), Powód, dla którego MyBaseException maleje. A może ma to chronić przed „naruszeniem warstwy”?
Yaroslav Nikitenko
6

Zobacz bardzo dobry artykuł „ Ostateczny przewodnik po wyjątkach w Pythonie ”. Podstawowe zasady to:

  • Zawsze dziedzicz po (przynajmniej) wyjątku.
  • Zawsze dzwoń BaseException.__init__tylko z jednym argumentem.
  • Podczas budowania biblioteki zdefiniuj klasę podstawową dziedziczącą po wyjątku.
  • Podaj szczegóły dotyczące błędu.
  • Dziedzicz po wbudowanych typach wyjątków, gdy ma to sens.

Są też informacje o organizowaniu (w modułach) i pakowaniu wyjątków, polecam przeczytać przewodnik.

Jarosław Nikitenko
źródło
1
To dobry przykład tego, dlaczego na SO zwykle sprawdzam najbardziej pozytywną odpowiedź, ale także najnowsze. Przydatne dodanie, dzięki.
logicOnAbstractions
1
Always call BaseException.__init__ with only one argument.Wydaje się, że jest to niepotrzebne ograniczenie, ponieważ w rzeczywistości akceptuje dowolną liczbę argumentów.
Eugene Yarmash
@EugeneYarmash Zgadzam się, teraz tego nie rozumiem. I tak go nie używam. Może powinienem ponownie przeczytać artykuł i rozszerzyć moją odpowiedź.
Yaroslav Nikitenko
@EugeneYarmash Przeczytałem ponownie artykuł. Stwierdzono, że w przypadku kilku argumentów implementacja C wywołuje „return PyObject_Str (self-> args);” Oznacza to, że jeden ciąg powinien działać lepiej niż kilka. Sprawdziłeś to?
Jarosław Nikitenko
3

Spróbuj tego przykładu

class InvalidInputError(Exception):
    def __init__(self, msg):
        self.msg = msg
    def __str__(self):
        return repr(self.msg)

inp = int(input("Enter a number between 1 to 10:"))
try:
    if type(inp) != int or inp not in list(range(1,11)):
        raise InvalidInputError
except InvalidInputError:
    print("Invalid input entered")
Omkaar.K
źródło
1

Aby poprawnie zdefiniować własne wyjątki, należy przestrzegać kilku najlepszych praktyk:

  • Zdefiniuj klasę podstawową dziedziczącą po Exception. Umożliwi to złapanie dowolnego wyjątku związanego z projektem (bardziej szczegółowe wyjątki powinny z niego dziedziczyć):

    class MyProjectError(Exception):
        """A base class for MyProject exceptions."""

    Organizowanie tych klas wyjątków w osobnym module (np. exceptions.py) Jest ogólnie dobrym pomysłem.

  • Aby przekazać dodatkowe argumenty do wyjątku, zdefiniuj niestandardową __init__()metodę ze zmienną liczbą argumentów. Wywołaj przekazanie przez klasę podstawową __init__()wszelkich argumentów pozycyjnych (pamiętaj, że BaseException/Exception spodziewaj się dowolnej liczby argumentów pozycyjnych ):

    class CustomError(MyProjectError):
        def __init__(self, *args, **kwargs):
            super(CustomError, self).__init__(*args)
            self.foo = kwargs.get('foo')

    Aby zgłosić taki wyjątek za pomocą dodatkowego argumentu, możesz użyć:

    raise CustomError('Something bad happened', foo='foo')

    Ten projekt w rzeczywistości jest zgodny z zasadą podstawienia Liskowa , ponieważ można zastąpić wystąpienie podstawowej klasy wyjątku wystąpieniem pochodnej klasy wyjątku.

Eugene Yarmash
źródło