Правильный способ объявить пользовательские исключения в современном Python?

1292

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

Под «современным Python» я подразумеваю что-то, что будет работать в Python 2.5, но будет «правильным» для Python 2.6 и Python 3. * способ ведения дел. Под «пользовательским» я подразумеваю объект Exception, который может включать дополнительные данные о причине ошибки: строку, возможно, также какой-либо другой произвольный объект, относящийся к исключению.

Я был сбит с толку следующим предупреждением об устаревании в 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__. Это похоже на много печатать, это необходимо?

нельсон
источник

Ответы:

1324

Может быть, я пропустил вопрос, но почему бы и нет

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: В 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
gahooa
источник
35
Однако такое исключение не может быть выбрано; см. обсуждение здесь stackoverflow.com/questions/16244923/…
jiakai
87
@jiakai означает «кирка». :-)
Робиньо
1
Следуя документации Python для пользовательских исключений, имена, упомянутые в функции __init__, являются неправильными. Вместо (self, message, error) это (self, expression, message). Выражение атрибута является входным выражением, в котором произошла ошибка, а сообщение является объяснением ошибки.
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
frnknstn
источник
2
«но это будет устаревшим в будущем» - это все еще предназначено для амортизации? Python 3.7 все еще радостно принимает Exception(foo, bar, qux).
mtraceur
Он не видел какой-либо недавней работы, чтобы ограничить его, так как последняя попытка не удалась из-за боли перехода, но это использование все еще не рекомендуется. Я обновлю свой ответ, чтобы отразить это.
frnknstn
@frnknstn, почему это не рекомендуется? Выглядит как хорошая идиома для меня.
Невес
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как рекомендуют документы). Кроме того, вы можете по крайней мере предоставить строку документации (и не быть вынужденным использовать 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

Опять же, проблема с вышеприведенным заключается в том, что для того, чтобы поймать его, вам нужно будет либо указать его конкретно (импортировать, если он создан в другом месте), либо перехватить Exception, (но вы, вероятно, не готовы обрабатывать все типы исключений, и вы должны ловить только исключения, которые вы готовы обработать). Критика аналогична приведенной ниже, но, кроме того, это не способ инициализации через 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

Таким образом, вы можете передать сообщение об ошибке второму параметру и перейти к нему позже с помощью e.errors

Это также требует, чтобы было передано ровно два аргумента (кроме 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 Критика Лискова о замещаемости остается в силе. Семантика первого аргумента как «сообщения» также сомнительна, но я не думаю, что буду спорить с этим. Я посмотрю на это больше, когда у меня будет больше свободного времени.
Aaron Hall
1
FWIW, для Python 3 (по крайней мере, для 3.6+) можно было бы переопределить __str__метод MyAppValueErrorвместо того, чтобы полагаться на messageатрибут
Jacquot
1
@AaronHall Не могли бы вы расширить преимущества подкласса ValueError, а не исключения? Вы утверждаете, что это то, что подразумевается под документами, но прямое чтение не поддерживает эту интерпретацию, и в Учебном руководстве по Python в разделе «Определенные пользователем исключения» оно явно делает выбор пользователя: «Исключения обычно должны быть получены из класса Exception, прямо или косвенно ". Поэтому, пожалуйста, постарайтесь понять, насколько ваш взгляд оправдан.
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
mykhal
источник
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, что иногда может раздражать при использовании инфраструктуры и необходимости отлавливать все возможные ошибки.

fameman
источник
1
По крайней мере, текущие документы указывают, что это способ сделать это (по крайней мере, без __str__), а не другие ответы, которые используют super().__init__(...).. Просто позор, который переопределяет __str__и __repr__, вероятно, необходим только для лучшей сериализации «по умолчанию».
Кевлар
2
Честный вопрос: почему важно, чтобы исключения были засолены? Каковы варианты использования для сброса и загрузки исключений?
Роэль Шровен,
1
@RoelSchroeven: мне приходилось распараллеливать код один раз. Хорошо выполнялся отдельный процесс, но аспекты некоторых его классов не были сериализуемыми (лямбда-функция передавалась как объекты). Мне потребовалось некоторое время, чтобы разобраться и исправить это. Это значит, что кому-то позже может понадобиться сериализовать ваш код, быть не в состоянии это сделать, и должен выяснить, почему ... Моя проблема не была неприметной ошибкой, но я вижу, что она вызывает аналогичные проблемы.
logicOnAbstractions
17

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

М.УТКУ АЛЬТИНКАЯ
источник
7

Нет, «сообщение» не запрещено. Это просто устарело. Ваше приложение будет нормально работать с использованием сообщений. Но вы можете избавиться от ошибки устаревания, конечно.

Когда вы создаете пользовательские классы Exception для своего приложения, многие из них не делятся на подклассы только из Exception, а из других, например ValueError или аналогичных. Затем вы должны адаптироваться к их использованию переменных.

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

try:
    ...
except NelsonsExceptions:
    ...

И в этом случае вы можете сделать __init__ and __str__ необходимое там, так что вам не нужно повторять это для каждого исключения. Но простой вызов переменной message, отличной от message, делает свое дело.

В любом случае, вам нужно только __init__ or __str__если вы делаете что-то отличное от того, что делает само Exception. А потому, что если начисляется амортизация, то вам нужны оба, иначе вы получите ошибку. Это не много дополнительного кода, который вам нужен для каждого класса. ;)

Леннарт Регебро
источник
Интересно, что исключения Django не наследуются от общей базы. 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 вызывает «return 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")
Omkaar.K
источник
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')

Этот дизайн придерживается принципа подстановки Лискова , так как вы можете заменить экземпляр базового класса исключений на экземпляр производного класса исключений. Кроме того, он позволяет создавать экземпляр производного класса с теми же параметрами, что и родительский.

Евгений Ярмаш
источник