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

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

Под "современным Python" я подразумеваю что-то, что будет работать в Python 2.5, но будет "правильным" для способов Python 2.6 и Python 3. *. И под "custom" я подразумеваю объект 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, но я никогда не знал, как его использовать. И я не уверен, что это правильный способ сделать то, что нужно сделать; многие обсуждения, которые я нашел в Интернете, предположили, что они пытались покончить с args в Python 3.

Обновление: два ответа предложили переопределить __init__ и __str__/__unicode__/__repr__. Это похоже на много типизирования, нужно ли это?

Ответы

Ответ 1

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

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

Таким образом, вы можете передать dict сообщений об ошибках второму параметру и перейти к нему позже с помощью 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

Ответ 2

С современными исключениями Python вам не нужно злоупотреблять .message или переопределять .__str__() или .__repr__() или любое из них. Если все, что вам нужно, это информативное сообщение при возникновении вашего исключения, сделайте это:

class MyException(Exception):
    pass

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

Это даст 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

Ответ 3

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

Это прекрасно, если только ваше исключение действительно является типом более конкретного исключения:

class MyException(Exception):
    pass

Или лучше (возможно, идеально), вместо pass введите docstring:

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

Подклассы исключений подклассов

Из docs

Exception

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

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

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

Задайте атрибуты, созданные вами с помощью __init__. Избегайте передачи dict в качестве позиционного аргумента, будущие пользователи вашего кода будут благодарны вам. Если вы используете атрибут устаревших сообщений, присваивая его самому, вы избежите 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

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

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

Также требуется передать только два аргумента (кроме self.) Нет больше, не меньше. Это интересное ограничение, которое будущие пользователи могут не оценить.

Чтобы быть прямым - это нарушает Liskov substitutability.

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

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

Ответ 4

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

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

Ответ 5

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

Ответ 6

Начиная с 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, что иногда может раздражать при использовании инфраструктуры и необходимости отлавливать все возможные ошибки.

Ответ 7

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

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

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

try:
    ...
except NelsonsExceptions:
    ...

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

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

Ответ 8

Попробуйте этот пример

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

Ответ 9

Смотрите очень хорошую статью " Полное руководство по исключениям Python ". Основные принципы:

  • Всегда наследовать от (по крайней мере) исключения.
  • Всегда вызывайте BaseException.__init__ только с одним аргументом.
  • При создании библиотеки определите базовый класс, унаследованный от Exception.
  • Укажите подробности об ошибке.
  • Наследовать от встроенных типов исключений, когда это имеет смысл.

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