Правильний спосіб оголосити спеціальні винятки в сучасному Python?


1289

Який правильний спосіб оголосити спеціальні класи виключень у сучасному Python? Моя основна мета - дотримуватися стандартних інших класів винятків, щоб (наприклад) будь-який додатковий рядок, який я включаю до винятку, був роздрукований будь-яким інструментом, що потрапив у виняток.

Під "сучасним Python" я маю на увазі те, що буде працювати в Python 2.5, але бути "правильним" для Python 2.6 та Python 3. * спосіб робити речі. Під "користувальницьким" я маю на увазі об'єкт "Виняток", який може включати додаткові дані про причину помилки: рядок, можливо, також якийсь інший довільний об'єкт, що має відношення до винятку.

Мене спрацьовувало таке попередження про знищення в Python 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

Здається, божевільним BaseExceptionє особливе значення для названих атрибутів message. Я знаходжу з PEP-352, що атрибут мав особливе значення в 2.5, яке вони намагаються знехтувати, тому я здогадуюсь, що це ім'я (і те саме) зараз заборонено? Тьфу.

Я також нечітко усвідомлюю, що Exceptionмає якийсь магічний параметр args, але я ніколи не знав, як ним користуватися. Я також не впевнений, що це правильний шлях робити справи вперед; Багато дискусій, які я знайшов в Інтернеті, припустили, що вони намагаються усунути аргументи в Python 3.

Оновлення: дві відповіді пропонують переосмислити __init__, та __str__/ __unicode__/ __repr__. Це здається, що багато друкувати, це потрібно?

Відповіді:


1322

Можливо, я пропустив питання, але чому б ні:

class MyException(Exception):
    pass

Редагувати: щоб щось замінити (або пропустити додаткові аргументи), зробіть це:

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

Таким чином, ви можете передати дік повідомлень про помилки до другого параметра і дійти до нього пізніше e.errors


Python 3 Update: У Python 3+ ви можете використовувати це трохи більш компактне використання 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

35
Однак виняток, визначений на зразок цього, не може бути зрозумілим; см обговорення тут stackoverflow.com/questions/16244923 / ...
Jiakai

86
@jiakai означає "соління". :-)
Робіно

1
Після документації python для визначених користувачем винятків імена, які згадуються у функції __init__, є невірними. Замість (себе, повідомлення, помилка) це (себе, вираз, повідомлення). Вираз атрибута - це вхідний вираз, в якому сталася помилка, а повідомлення є поясненням помилки.
ddleon

2
Це непорозуміння, @ddleon. Приклад у документах, на які ви посилаєтесь, стосується конкретного випадку використання. Немає значення для аргументів конструктора підкласу (а також їх кількості).
asthasr

498

В сучасних винятків Python, вам не потрібно зловживати .message, або перевизначити .__str__()або .__repr__()або який - або з нього. Якщо все, що ви хочете, - це інформативне повідомлення при підвищенні вашого винятку, зробіть це:

class MyException(Exception):
    pass

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

Це дасть прослідку, що закінчується MyException: My hovercraft is full of eels.

Якщо ви хочете отримати більшу гнучкість від винятку, ви можете передати словник як аргумент:

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

Однак дістатись до цих деталей у exceptблоці трохи складніше. Деталі зберігаються в argsатрибуті, який є списком. Вам потрібно буде зробити щось подібне:

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

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

class MyError(Exception):
    def __init__(self, message, animal):
        self.message = message
        self.animal = animal
    def __str__(self):
        return self.message

2
"але це буде застарілим у майбутньому" - це все ж призначене для депресії? Python 3.7 все ще, здається, радісно приймає Exception(foo, bar, qux).
mtraceur

Вона не бачила жодної недавньої роботи, щоб видалити її, оскільки остання спроба не вдалася через біль при переході, але це використання все ще не рекомендується. Я оновлю свою відповідь, щоб це відобразити.
frnknstn

@frnknstn, чому це відлякує? Виглядає як приємна ідіома для мене.
neves

2
@neves для початку, використання кортежів для зберігання інформації про винятки не має ніякої користі, ніж використання словника для того ж. Якщо вас цікавлять міркування щодо змін винятків, подивіться на PEP352
frnknstn

Відповідний розділ PEP352 - " Відтягнуті ідеї" .
liberforce

196

"Правильний спосіб оголосити спеціальні винятки в сучасному Python?"

Це добре, якщо виняток не є типом більш конкретного винятку:

class MyException(Exception):
    pass

Або краще (можливо, ідеально), замість того, щоб passнадати доктрину:

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

Підкласи винятків підкласів

Від док

Exception

Усі вбудовані винятки, що не входять в систему, походять з цього класу. Усі винятки, визначені користувачем, також повинні походити з цього класу.

Це означає, що якщо ваш виняток є типом більш конкретного винятку, підклас цього винятку замість загального Exception(і результат буде таким, який ви все ще отримуєте з Exceptionрекомендацій, які рекомендують документи). Крім того, ви можете принаймні надати docstring (і не бути змушеним використовувати passключове слово):

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

Встановлюйте атрибути, які ви створюєте самостійно за допомогою спеціального користування __init__. Уникайте передачі дикту як позиційного аргументу, майбутні користувачі вашого коду будуть вам вдячні. Якщо ви використовуєте атрибут застарілого повідомлення, призначаючи його самостійно, ви уникнете 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) 

Насправді не потрібно писати свій власний __str__або __repr__. Вбудовані дуже приємні, і ваше спільне спадщину гарантує, що ви ним користуєтесь.

Критика верхньої відповіді

Можливо, я пропустив питання, але чому б ні:

class MyException(Exception):
    pass

Знову ж таки, проблема вищезазначеного полягає в тому, що для того, щоб зловити його, вам доведеться або назвати його конкретно (імпортувати його, якщо створено в іншому місці), або вилучити виняток (але ви, мабуть, не готові обробляти всі типи винятків, і ви повинні виловлювати лише винятки, з якими ви готові впоратися). Аналогічна критика нижче, але крім того, це не спосіб ініціалізації через super, і ви отримаєте, DeprecationWarningякщо ви отримаєте доступ до атрибуту повідомлення:

Редагувати: щоб щось замінити (або пропустити додаткові аргументи), зробіть це:

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

Таким чином, ви можете передати дік повідомлень про помилки до другого параметра та дістатись до нього пізніше за допомогою ererrors

Він також вимагає передавати рівно два аргументи (окрім self.) Не більше, не менше. Це цікаве обмеження, яке майбутні користувачі можуть не оцінити.

Бути прямим - це порушує замість Ліскова .

Я продемонструю обидві помилки:

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

У порівнянні з:

>>> MyAppValueError('foo', 'FOO', 'bar').message
'foo'

2
Привіт з 2018 року! BaseException.messageнемає в Python 3, тому критика стосується лише старих версій, правда?
Кос

5
@Kos Критика щодо заміщення Ліскова досі діє. Семантика першого аргументу як "повідомлення" також сумнівно сумнівна, але я не думаю, що я буду сперечатися з цим. Я приділяю цьому більше уваги, коли матиму більше вільного часу.
Аарон Холл

1
FWIW, для Python 3 (принаймні для 3.6+) можна було б переосмислити __str__метод MyAppValueErrorзамість того, щоб покластися на messageатрибут
Jacquot

1
@AaronHall Чи можете ви розширити перевагу підкласингу ValueError, а не винятку? Ви стверджуєте, що саме це розуміється під документами, але пряме читання не підтримує цю інтерпретацію, а в Інструкції з Python в розділі "Виключення", визначеного користувачем, це чітко робить вибір користувачів: "Винятки, як правило, мають бути отримані з класу" Винятки ", прямо чи опосередковано ". Отже, прагнете зрозуміти, чи виправданий ваш погляд, будь ласка.
ostergaard

1
@ostergaard Не можу відповісти повністю повністю, але коротше кажучи, користувач отримує додатковий варіант лову ValueError. Це має сенс, якщо він знаходиться в категорії помилок вартості. Якщо це не відноситься до категорії помилок значення, я б заперечував проти цього на семантиці. З боку програміста є місце для деяких нюансів і міркувань, але я дуже вважаю за краще специфіку, коли це стосується. Я оновлю свою відповідь, щоб швидше вирішити цю тему.
Аарон Холл

50

подивіться, як винятки працюють за замовчуванням, якщо використовуються один атрибут порівняно з кількома (відслідковуються відстеження):

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

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

тож, можливо, ви хочете мати якийсь " шаблон винятку ", який працює як сам виняток, сумісним чином:

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

це можна легко зробити за допомогою цього підкласу

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

і якщо вам не подобається це стандартне подання за кордоном, просто додайте __str__метод до ExceptionTemplateкласу, наприклад:

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

і ти матимеш

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

32

Станом на Python 3.8 (2018, https://docs.python.org/dev/whatsnew/3.8.html ), рекомендований метод як і раніше:

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

Будь ласка, не забудьте задокументувати, чому необхідний спеціальний виняток!

Якщо вам потрібно, це шлях для винятку з більшою кількістю даних:

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

і отримувати їх, як:

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=Noneважливо зробити його маринованим. Перш ніж скинути його, вам потрібно зателефонувати error.__reduce__(). Завантаження працюватиме як очікувалося.

Можливо, вам слід дослідити пошук рішення за допомогою returnоператора pythons, якщо вам потрібно багато даних для передачі до якоїсь зовнішньої структури. Це здається мені більш чітким / пітонічним. Розширені винятки сильно використовуються в Java, що іноді може дратувати, коли використовує рамки і потрібно вловлювати всі можливі помилки.


1
Принаймні, поточні документи вказують, що це спосіб зробити це (принаймні, без __str__), а не інші відповіді, які використовують super().__init__(...).. Просто ганьба, що перекриває __str__і __repr__, ймовірно, потрібна лише для кращої серіалізації "за замовчуванням".
kevlarr

2
Чесне запитання: Чому важливо, щоб винятки мали можливість підбирати соління? Які випадки використання для демпінгу та завантаження винятків?
Roel Schroeven

1
@RoelSchroeven: Мені довелося один раз паралелізувати код. Однозначний процес пройшов непогано, але аспекти деяких його класів не були серіалізаційними (функція лямбда передається як об'єкти). Взяв у мене деякий час, щоб з'ясувати це і виправити. Це означає, що пізніше хтось може призвести до необхідності серіалізації вашого коду, не в змозі це зробити, і доведеться викопати, чому ... Моя проблема не була помилковою помилкою, але я можу помітити, що це викликає подібні проблеми.
logicOnAbstractions

17

Вам слід замінити __repr__або __unicode__методи замість того, щоб використовувати повідомлення, аргументи, які ви надаєте під час створення винятку, будуть в argsатрибуті об'єкта виключення.


7

Ні, "повідомлення" не заборонено. Це просто застаріло. Ваша програма буде добре працювати з використанням повідомлення. Але, можливо, ви захочете позбутися помилки депресації, звичайно.

Коли ви створюєте власні класи винятків для вашої програми, багато з них не підкласи просто з винятку, а з інших, наприклад ValueError або подібних. Тоді вам доведеться адаптуватися до їх використання змінних.

І якщо у вашій програмі є багато винятків, зазвичай є хорошою ідеєю створити загальний для кожного з них базовий клас, щоб користувачі ваших модулів могли робити

try:
    ...
except NelsonsExceptions:
    ...

І в цьому випадку ви можете зробити це __init__ and __str__ необхідне там, тому вам не доведеться повторювати це за кожним винятком. Але просто виклик змінної повідомлення чимось іншим, ніж повідомлення, робить свою справу.

У будь-якому випадку, вам потрібно лише, __init__ or __str__якщо ви зробите щось інше, ніж те, що робить сам виняток. А тому що якщо депресія, то вам потрібно обоє, або ви отримаєте помилку. Це ще не багато зайвого коду, який вам потрібен для кожного класу. ;)


Цікаво, що винятки Джанго не успадковують із загальної бази. docs.djangoproject.com/en/2.2/_modules/django/core/exceptions Чи є у вас хороший приклад, коли потрібні вилучення всіх винятків із конкретної програми? (можливо, це корисно лише для деяких конкретних типів програм).
Ярослав Нікітенко

Я знайшов хорошу статтю на цю тему, julien.danjou.info/python-exceptions-guide . Я думаю, що Винятки слід підкласифікувати насамперед на основі домену, а не на основі додатків. Якщо у вашому додатку є протокол HTTP, ви отримуєте HTTPError. Коли частиною вашого додатка є TCP, ви отримуєте виключення цієї частини з TCPError. Але якщо ваша програма охоплює багато доменів (файлів, дозволів тощо), причина для MyBaseException зменшується. Або це захист від "порушення шару"?
Ярослав Нікітенко

6

Дивіться дуже гарну статтю " Остаточний посібник з винятків Python ". Основні принципи:

  • Завжди успадковуйте (принаймні) Виняток.
  • Завжди дзвоніть BaseException.__init__лише з одним аргументом.
  • Створюючи бібліотеку, визначте базовий клас, успадкований від Exception.
  • Надайте детальну інформацію про помилку.
  • Наслідуйте вбудовані типи винятків, коли це має сенс.

Також є інформація про впорядкування (у модулях) та обгортання винятків, рекомендую прочитати посібник.


1
Це хороший приклад того, чому на SO я зазвичай перевіряю найбільш схвалену відповідь, але і останню. Корисне доповнення, дякую.
logicOnAbstractions

1
Always call BaseException.__init__ with only one argument.Здається, це зайве обмеження, оскільки воно фактично приймає будь-яку кількість аргументів.
Євген Ярмаш

@EugeneYarmash Я згоден, зараз я цього не розумію. Я все одно не використовую. Можливо, я повинен перечитати статтю та розширити свою відповідь.
Ярослав Нікітенко

@EugeneYarmash Я знову прочитав статтю. Зазначається, що у випадку декількох аргументів реалізація C викликає "повернути PyObject_Str (self-> args);" Це означає, що одна струна повинна працювати краще, ніж кілька. Ви це перевірили?
Ярослав Нікітенко

3

Спробуйте цей приклад

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

1

Щоб правильно визначити власні винятки, є кілька найкращих практик, яких потрібно дотримуватися:

  • Визначте базовий клас, який успадковується від Exception. Це дозволить знайти будь-які винятки, пов'язані з проектом (більш конкретні винятки повинні успадковувати його):

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

    Організувати ці класи виключень в окремий модуль (наприклад exceptions.py), як правило, хороша ідея.

  • Щоб передати додаткові аргументи (аргументи) до вашого винятку, визначте власний __init__()метод із змінною кількістю аргументів. Викличте базовий клас, __init__()передавши йому будь-які позиційні аргументи (пам’ятайте, що BaseException/Exception очікуйте будь-яку кількість позиційних аргументів ):

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

    Щоб підняти такий виняток за допомогою додаткового аргументу, ви можете використовувати:

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

    Ця конструкція фактично дотримується до принципу заміщення Лісков , так як ви можете замінити екземпляр базового класу винятків з екземпляром похідного класу винятку.

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