Что такое кастомная ошибка

В Python имеется так много встроенных исключений, что программисты редко нуждаются в создании и использовании пользовательских исключений. Или это не так?

Какие исключения стоит применять — пользовательские или встроенные? Это, на самом деле, очень хороший вопрос. Одни отвечают на него так:

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

А другие — так:

Применяйте пользовательские исключения в своих проектах. Оставьте встроенные исключения для типичных ситуаций, в которых они генерируются. Пользовательские исключения вызывайте для выдачи информации о том, что что-то пошло не так в связи с приложением, а не с самим кодом.

Авторы книги «Python. Искусственный интеллект, большие данные и облачные вычисления» Пол Дейтел и Харви Дейтел утверждают, что программисту следует пользоваться встроенными исключениями. Автор книги «Clean Python: Elegant Coding in Python» Сунил Капиль рекомендует применять пользовательские исключения при создании интерфейсов или библиотек, так как подобные исключения помогают диагностировать проблемы, произошедшие в коде. Так же считает и автор книги «Python Tricks: A Buffet of Awesome Python Features» Дэн Бадер, обосновывая это тем, что пользовательские исключения помогают пользователям тогда, когда код следует стратегии EAFP (Easier to ask for forgiveness than permission, проще просить прощения, чем получить разрешение).

Если вас мучают сомнения по поводу разных видов исключений, или если вам просто интересны пользовательские исключения — значит, эта статья написана специально для вас. Здесь мы поговорим о том, стоит или не стоит применять пользовательские исключения в своих проектах вместо того, чтобы всегда, когда это возможно (то есть, в общем-то, просто «всегда»), прибегать к встроенным исключениям. Прежде чем мы двинемся дальше — обратите внимание на то, что мы не будем пытаться сделать выбор между «правильным» и «неправильным». Мы, скорее, будем заниматься поисками золотого правила, которое поможет нам найти правильный баланс исключений.

Определение пользовательского исключения

Для начала поговорим о том, как определить пользовательское исключение в Python. Это просто — достаточно создать класс, наследующий от встроенного класса Exception:

>>> class PredictionError(Exception):
...     pass

Как вы увидите ниже, при определении пользовательского исключения можно сделать не только это; но суть в том, что в большинстве случаев я использую лишь пустой класс (на самом деле — не пустой, так как он является наследником Exception), так как это — всё, что мне нужно. Часто я добавляю к такому классу описательную строку документации:

>>> class PredictionError(Exception):
...     """Error occurred during prediction."""

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

Обратите внимание на важность правильного именования исключений. Пользовательское исключение PredictionError (ошибка в прогнозе) выглядит как довольно универсальное, общее исключение, которое подойдёт для случаев, когда что-то идёт не так при нахождении некоего прогноза. Можно, в зависимости от ваших нужд, создавать более конкретные исключения. Но, в любом случае, всегда помните о том, что исключениям надо давать информативные имена. В Python существует общее правило, в соответствии с которым имена сущностей должны быть короткими, но информативными. По иронии судьбы, пользовательские исключения являются исключением из этого правила, так как они часто имеют длинные имена. Это так из-за того, что большинство людей — включая меня — предпочитают использовать самодостаточные имена исключений. Полагаю, вам тоже стоит придерживаться этого правила. Взгляните на следующие пары имён исключений — информативных и недостаточно ясных:

  • NegativeValueToBeSquaredError в сравнении с SquaredError.

  • IncorrectUserNameError в сравнении с InputError.

  • OverloadedTruckError и NoLoadOnTruckError в сравнении с LoadError.

Имена, находящиеся слева, более специфичны и информативны, чем те, которые находятся справа. С другой стороны, «правые» имена можно рассматривать как общие ошибки, наследниками которых могут быть исключения, имена которых находятся слева. Например:

class LoadError(Exception):
    """General exception to be used when there is an error with truck load."""

class OverloadTruckError(LoadError):
    """The truck is overloaded."""

class NoLoadOnTruckError(LoadError):
    """The truck is empty."""

Это называют иерархией исключений. У встроенных ошибок тоже имеется иерархия. Иерархия исключений может служить важной цели: когда создают такую иерархию, пользователю необязательно знать обо всех конкретных исключениях (из книги Марка Лутца «Изучаем Python»). Вместо этого достаточно знать и перехватывать общие исключения (в нашем примере это — LoadError); это позволит перехватывать все исключения, наследующие от них (OverloadTruckError и NoLoadOnTruckError). Автор книги «Clean Python: Elegant Coding in Python» Сунил Капиль эту рекомендацию подтверждает, но он предупреждает читателя о том, что такую иерархию не следует делать слишком сложной.

Иногда, правда, достаточно пойти самым простым путём:

class OverloadTruckError(Exception):
    """The truck is overloaded."""

class NoLoadOnTruckError(Exception):
    """The truck is empty."""

Если вы думаете, что исключение NoLoadOnTruckError (сообщающее о том, что в грузовике ничего нет) не должно рассматриваться как ошибка, так как грузовики иногда ездят и пустыми, то вы правы. Но помните о том, что исключения и не обязаны олицетворять собой ошибки. Их возникновение означает… возникновение исключительной ситуации. Правда, по правилам Python в конце имён классов исключений должно быть слово Error, так устроены имена всех встроенных исключений (например — ValueError или OSError).

Выдача пользовательского исключения

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

Выдача исключения после проверки условия

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

# без сообщения
if load == 0:
    raise NoLoadOnTruckError

# с сообщением
truck_no = 12333
if load == 0:
    raise NoLoadOnTruckError(f"No load on truck {truck_no}")

Перехват встроенного исключения и выдача пользовательского исключения

class EmptyVariableError(Exception):
    pass

def get_mean(x):
    return sum(x) / len(x)

def summarize(x):
    try:
        mean = get_mean(x)
    except ZeroDivisionError:
        raise EmptyVariableError
    total = sum(x)
    n = len(x)

Здесь, вместо выдачи ZeroDivisionError, мы выдаём пользовательское исключение EmptyVariableError. С одной стороны — этот подход может быть более информативным, так как он позволяет сообщить пользователю о сути возникшей проблемы. С другой стороны — он не позволяет сообщить пользователю всю информацию. То есть, иными словами, выдача только исключения EmptyVariableError не сообщит пользователю о том, что переменная не содержала значение, и по этой причине произошло деление на ноль при вычислении среднего значения с использованием функции get_mean(). Разработчику программы нужно решить — должен ли её пользователь знать такие обширные подробности о её работе. Иногда это не нужно. Но в некоторых случаях оказывается так, что чем больше сведений содержится в отчётах о трассировке стека — тем лучше.

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

Перехват встроенного исключения и выдача из него пользовательского исключения

class EmptyVariableError(Exception):
    pass

def get_mean(x):
    return sum(x) / len(x)

def summarize(x):
    try:
        mean = get_mean(x)
    except ZeroDivisionError as e:
        raise EmptyVariableError from e
    total = sum(x)
    n = len(x)

Тут в отчёт о трассировке стека попадает и EmptyVariableError, и ZeroDivisionError. Здесь, в сравнении с предыдущим примером, изменено всего две строчки.

Раньше было так:

    except ZeroDivisionError:
        raise EmptyVariableError

Теперь стало так:

    except ZeroDivisionError as e:
        raise EmptyVariableError from e

Эта версия кода даёт гораздо более информативные сведения пользователю, так как сообщает ему больше деталей: о том, что переменная была пустой и что в ней не было данных, о том, что из-за этого было выдано исключение ZeroDivisionError при вычислении среднего значения в функции get_mean(). Даёт ли ту же информацию сообщение вида ZeroDivisionError: division by zero? Напрямую об этом тут, понятно, не сообщается. Чтобы получить те же сведения, нужно тщательно проанализировать отчёт о трассировке стека.

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

Расширение возможностей пользовательских исключений

До сих пор мы использовали пользовательские исключения, выполняя лишь минимальные их настройки. Несмотря на простоту такого подхода, он отличается широкими возможностями настройки исключений благодаря тому, что при выдаче исключения можно использовать любое сообщение. Но можно применять сообщения, встроенные в класс исключения. А значит — при выдаче исключения передавать ему сообщение не нужно. Взгляните на этот код:

class NoLoadOnTruckError(Exception):
    """The truck is empty."""
    def __init__(self, truck_no=None):
        self.msg = f"The {truck_no} is empty" if truck_no else ""    
    def __str__(self):
        return self.msg
        

truck_no = 12333
if load == 0:
    raise NoLoadOnTruckError(truck_no)

Получается, что если не передать исключению значение truck_no, это исключение сообщение не выводит. А если передать это значение (например — 12333) — исключение NoLoadOnTruckError будет выдано с сообщением «The truck 12333 is empty». Это — простой пример, подробнее об этом можно почитать здесь.

Я применяю этот подход лишь в одной ситуации: тогда, когда иначе мне пришлось бы использовать одно и то же длинное сообщение в нескольких местах кода при выдаче некоего исключения. В результате появляется смысл в расширении класса этого исключения за счёт встраивания в него текста сообщения. Но я стараюсь, чтобы подобные классы были бы устроены настолько просто, насколько это возможно, стремясь не усложнять их код.

В других ситуациях, на самом деле — в большинстве случаев, я просто использую пустой класс, наследующий от базового класса Exception. Это — самое простое решение, дающее мне то, что мне нужно. Классы исключений, возможности которых расширены за счёт встроенных сообщений, могут оказаться куда сложнее «пустых» классов исключений, и это при том, что они не дают пользователю каких-то особых возможностей. К тому же — такие классы, как в вышеприведённом примере, нельзя подвергать дальнейшей настройке. При выдаче исключения можно либо опустить сообщение, либо использовать то, что встроено в класс, а использовать что-то другое нельзя. Чтобы сделать это возможным, класс придётся усложнить ещё сильнее.

Пример

Теперь, когда вы знаете о том, как определять пользовательские исключения, пришло время увидеть их в деле.

>>> import datetime
>>> from typing import List
>>> TimeSeriesDates = List[datetime.datetime.date]
>>> TimeSeriesValues = List[float]
>>> class IncorrectTSDataError(Exception):
...     """Incorrect data, a forecasting model cannot be built."""
>>> def build_model(ts: TimeSeriesDates, y: TimeSeriesValues):
...     if not ts:
...         raise IncorrectTSDataError("No dates available for time series")
...     if not y:
...         raise IncorrectTSDataError("No y values available for time series")
...     if len(ts) != len(y):
...         raise IncorrectTSDataError("Values (y) and dates (ts) have different lengths")
...     try:
...         model = run_model(ts, y)
...     except Exception as e:
...         raise PredictionError("Error during the model-building process") from e
...     return model

Для начала обратите внимание на то, что я поменял форматирование представленного кода. Теперь команды начинаются с >>>. Именно так форматируют код и результаты его работы при использовании стандартного Python-модуля doctest, применяемого для тестирования документации. Модуль doctest позволяет «запускать» документы, проверяя приведённые в них примеры кода, при его использовании, кроме того, легко отличать код от формируемых им выходных данных.

Я решил использовать в аннотациях типов псевдонимы типов. Я полагаю, что подобные аннотации типов отличаются лучшей читабельностью, чем сложные аннотации, включённые непосредственно в сигнатуру функции. В результате у нас имеются типы TimeSeriesDates и TimeSeriesValues. Оба — это списки. Первый — список объектов datetime.datetime.date, а второй — список чисел с плавающей запятой.

Далее — я создал пользовательское исключение IncorrectTSDataError, которое планируется использовать в том случае, если данные временной последовательности некорректны. Обратите внимание на то, что мы используем один и тот же класс исключения в трёх различных ситуациях, в которых данные являются некорректными. Мы могли бы создать иерархию исключений с тремя пользовательскими исключения, классы которых являются наследниками IncorrectTSDataError. Но мы ограничились одним исключением, а в вашем проекте, возможно, лучше покажет себя более сложная иерархия исключений.

Затем объявляется главная функция для построения модели — build_model(). Это, конечно, функция упрощённая. Выполняет она всего два действия:

  • Проверяет — корректны ли данные, а именно: имеются ли в распоряжении функции списки значений ts и y, и имеют ли эти списки одинаковую длину.

  • Строит модель (представленную функцией run_model()).

Мы могли бы переместить код проверок в выделенную функцию (например — check_data()) и, определённо, сделали бы это, если бы код функции build_model() стал бы гораздо длиннее.

Если одна из проверок завершится неудачно, функция выбросит исключение IncorrectTSDataError с сообщением, зависящим от того, что именно пошло не так. В противном случае функция продолжит работу и вызовет функцию run_model(). Конечно, наши проверки данных чрезмерно упрощены, они нужны лишь для демонстрационных целей. Мы могли бы проверить, действительно ли данные являются списком значений datetime.datetime.date; могли бы проверить, достаточно ли у нас точек данных для построения модели прогнозирования; могли бы провести и другие подобные проверки.

А теперь обратите внимание на то, как именно мы вызываем функцию run_model(). Мы это делаем, применяя блок try-except — чтобы иметь возможность перехватить любую ошибку, выброшенную в ходе выполнения функции. Если мы перехватим ошибку — наш код не станет молча её «проглатывать» — мы выдадим её повторно, выдав из неё исключение PredictionError, воспользовавшись конструкцией raise PredictionError from e. Я, ради простоты, не использовал сообщение при выдаче этого исключения. При таком подходе в отчёт о трассировке стека будет включена исходная ошибка.

Для того чтобы всё это запустить и посмотреть, как это работает, нам нужна функция run_model(). Создадим её мок (mock, имитацию реализации), который не делает ничего, кроме выдачи ошибки (здесь — это ValueError).

Мок объекта — это его искусственное представление, которое имитирует поведение исходного объекта, в данном случае — функции run_model(). Смысл использования мок-объекта в том, что нам не нужен весь объект (функция), а лишь его модель, которая имитирует интересующие нас аспекты его поведения. В нашем случае моку достаточно выдать исключение ValueError.

>>> def run_model(ts: TimeSeriesDates, y: TimeSeriesValues):
...     raise ValueError("Something went wrong")

Таким образом, всякий раз, когда мы запускаем функцию, она выдаёт ValueError:

>>> run_model([], [])
Traceback (most recent call last):
    ...
ValueError: Something went wrong

Это — наше приложение в действии. Мы сгенерируем значения из одномерного распределения, поэтому в них вряд ли будет присутствовать тренд.

>>> from random import uniform
>>> first_date = datetime.datetime.strptime("2020-01-01", "%Y-%m-%d")
>>> ts = [first_date + datetime.timedelta(i) for i in range(366)]
>>> y = [uniform(-50, 150) for _ in range(1, 367)]
>>> ts[-1]
datetime.datetime(2020, 12, 31, 0, 0)
>>> build_model([], y)
Traceback (most recent call last):
    ...
IncorrectTSDataError: No dates available for time series

>>> build_model(ts, [])
Traceback (most recent call last):
    ...
IncorrectTSDataError: No y values available for time series

>>> build_model(ts, y[:200])
Traceback (most recent call last):
    ...
IncorrectTSDataError: Values (y) and dates (ts) have different lengths

>>> build_model(ts, y) #doctest: +ELLIPSIS
Traceback (most recent call last):
    ...
PredictionError: Error during the model-building process

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

Traceback (most recent call last):
    File "...", line 5, in build_model
    model = run_model(ts, y)
    File "...", line 2, in run_model
    raise ValueError("Something went wrong")
ValueError: Something went wrong

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
    File "...", line 1336, in __run
    exec(compile(example.source, filename, "single",
    File "...", line 1, in <module>
    build_model(ts, y)
    File "...", line 7, in build_model
    raise PredictionError("Error during the model-building process") from e
PredictionError: Error during the model-building process

А теперь посмотрим на то, как выглядел бы отчёт о трассировке стека в том случае, если бы вместо исключения PredictionError было бы выдано встроенное исключение. Для того чтобы это сделать — сначала надо поменять код функции build_model():

>>> def build_model(ts: TimeSeriesDates, y: TimeSeriesValues):
...     model = run_model(ts, y)
...     return model

Эта версия функции build_model(), на самом деле, не имеет особого смысла, так как она просто вызывает run_model(). Она, правда, могла бы делать куда больше. Например — могла бы проверять данные или выполнять предварительную обработку данных.

Посмотрим на отчёт о трассировке стека, выполняемый в тех же самых условиях, которые рассматривались выше:

>>> build_model([], y)
Traceback (most recent call last):
    ...
ValueError: Something went wrong

>>> build_model(ts, y) #doctest: +ELLIPSIS
Traceback (most recent call last):
    File "...", line 1336, in __run
    exec(compile(example.source, filename, "single",
    File "...", line 1, in <module>
    build_model(ts, y) #doctest: +ELLIPSIS
    File "...", line 2, in build_model
    model = run_model(ts, y)
    File "...", line 2, in run_model
    raise ValueError
ValueError: Something went wrong

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

Слева показан отчёт о трассировке стека при применении пользовательского исключения. Справа — при использовании встроенного исключения ValueError (изображение подготовлено автором материала)

Слева показан отчёт о трассировке стека при применении пользовательского исключения. Справа — при использовании встроенного исключения ValueError (изображение подготовлено автором материала)

Обратите внимание на следующее:

  • Отчёт о трассировке стека, полученный при применении пользовательского исключения, содержит сведения об исходной ошибке (ValueError), но раскрывает эти сведения, используя пользовательское исключение PredictionError с заданным нами сообщением.

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

Согласны ли вы со мной в том, что отчёт о трассировке стека, полученный с помощью пользовательского исключения, получился понятнее?

Синтаксическая конструкция raise MyException from AnotherExcepion — это чрезвычайно мощный инструмент, так как он позволяет программисту показать и отчёт о трассировке стека исходного исключения, и отчёт трассировки стека пользовательского исключения. При таком подходе отчёты о трассировке стека могут быть гораздо информативнее в плане выяснения того, что именно привело к возникновению ошибки, чем отчёты, полученные только при использовании встроенных исключений.

Итоги

Пользовательские исключения легко создавать — особенно, когда программист не усложняет себе жизнь добавлением в их код методов .init() и .str(). Это — одна их тех довольно редких ситуаций, о которых можно сказать так: «меньше кода — больше функционала». В большинстве случаев это означает, что при создании пользовательского исключения лучше всего воспользоваться пустым классом, унаследованным от Exception.

Но, кроме того, при использовании пустого класса, нужно решить — писать ли код строки документации. (Решение заключается в том — использовать ли выражение pass, или многоточие не имеет никакого смысла, то есть — его можно проигнорировать). Ответ на этот вопрос зависит от каждой конкретной ситуации. Зависит он от того, что программист ожидает от своего класса. Если это должен быть универсальный класс, предназначенный для обслуживания нескольких исключений, тогда может понадобиться строка документации, сообщающая об этом. Правда, если выбран именно этот вариант — надо подумать о том, что, возможно, несколько более узконаправленных классов исключений покажут себя лучше, чем один универсальный. Я не утверждаю, что так оно и будет, так как во многих случаях разработчики не стремятся применять по 50 пользовательских исключений. В отличие от 100-долларовых купюр в бумажнике, в случае с исключениями, иногда 5 лучше, чем 50.

Тут имеет значение и ещё кое-что: хорошее, информативное имя исключения. Если воспользоваться хорошим именем, класс исключения может оказаться самодостаточным даже без строки документации и без сообщения. Но даже если без строки документации и без сообщения не обойтись, пользовательским исключениям, всё равно, нужны хорошие имена. Отрадно то, что классы исключений могут иметь более длинные имена, чем типичные Python-объекты. Поэтому, в случае с исключениями, имена вроде IncorrectAlphaValueError и MissingAlphaValueError даже не кажутся слишком длинными.

И ещё, меня прямо-таки восхищает конструкция raise from, которая позволяет выдавать пользовательские исключения из исключений, вызываемых внутри кода. Это позволяет включать в отчёт о трассировке стека два блока информации. Первый — имеющий отношение к изначально выданному исключения (оно, правда, не обязательно должно быть встроенным), который сообщает о том, что и где произошло; мы можем называть его базовым источником ошибки. Второй блок — это то, что относится к нашему пользовательскому исключению, который, на самом деле, раскрывает пользователю сведения о том, что именно произошло, пользуясь терминами, имеющими отношение к конкретному проекту. Комбинация этих двух блоков информации превращает отчёты о трассировке стека в серьёзный инструмент разработчика.

Правда, у разных проектов есть свои особенности. Разные подходы к применению пользовательских исключений используются в работе над пакетами, предназначенными для других программистов, над бизнес-проектами, над чем-то своим (вроде Jupyter-блокнота с каким-нибудь отчётом). Объясню подробнее:

  • Пакет, предназначенный для использования другими программистами. Таким пакетам применение пользовательских исключений часто идёт на пользу. Эти исключения должны быть хорошо спроектированными, пользоваться ими нужно с умом — чтобы они могли бы точно сообщать о том, что и где пошло не так.

  • Бизнес-проект. В подобных проектах пользовательские исключения — это, обычно, стоящее вложение сил. Встроенные исключения дают сведения о проблемах, имеющих отношение к Python, а пользовательские исключения добавляют к этим сведениям данные о проблемах, имеющих отношение к конкретному проекту. При таком подходе код (и то, что попадает в отчёт о трассировке стека при выдаче исключения) можно проектировать так, что при возникновении проблем пользователь проекта узнает не только о неполадках общего характера, но и о специфических проблемах проекта.

  • Код для небольших собственных проектов, вроде того, что пишут в Jupyter-блокнотах. Это может быть и код некоего скрипта, или даже фрагмент кода, который планируется использовать не больше пары раз. Чаще всего в таких случаях пользовательские исключения — это уже перебор, они безосновательно усложняют код. Блокноты обычно не нуждаются в мощных системах обработки исключений. Поэтому в подобных случаях программисты редко нуждаются в пользовательских исключениях.

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

Надеюсь, вам понравилась эта статья. Почему-то во многих книгах эту тему незаметно обходят вниманием, даже не упоминая о пользовательских исключениях. Есть, конечно, книги, вроде тех, что я упоминал в начале статьи, к которым вышесказанное не относится. Но, несмотря на это, у меня возникает такое ощущение, что многие авторы не считают пользовательские исключения темой, достаточно интересной или важной для того, чтобы её обсуждать. Я придерживаюсь прямо противоположного мнения по той простой причине, что нашёл пользовательские исключения полезными в порядочном количестве проектов. Если кто-то ещё даже не пробовал пользовательские исключения — возможно, пришло время их попробовать и проверить их на своём проекте.

На мой взгляд, пользовательские исключения предлагают нам фантастический инструмент для диагностирования проблем. Python известен хорошей читабельностью кода и дружественностью по отношению к пользователю. Применение пользовательских исключений может помочь нам сделать Python ещё читабельнее и ещё дружелюбнее, особенно — при разработке собственных пакетов. Если вы решили любой ценой избегать пользовательских исключений, используя лишь встроенные исключения, вы рискуете ухудшить читабельность Python-кода.

Поэтому я надеюсь, что с этого момента тот, кто боялся создавать классы собственных исключений, избавится от этого страха. Когда вы будете работать над интерфейсом или пакетом — не бойтесь создавать вложенные иерархии классов исключений. Иногда, правда, пользовательские исключения вам не понадобятся. Смысл тут в том, что всегда, прежде чем их применять, стоит поразмыслить о том — способны ли они принести пользу конкретному проекту. Если вы решите, что они вам нужны — помните о том, что не стоит злоупотреблять сложными конструкциями, и никогда не забывайте о «Дзен Python»: «Простое лучше, чем сложное», «Плоское лучше, чем вложенное»

О, а приходите к нам работать? 🤗 💰

Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.

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

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде.

In this article, we will learn to create custom errors with some examples.

Errors represent the state of being wrong in condition. Javascript handles a predefined set of errors by itself but if you want to create your own error handling mechanism you can do that with custom errors functionality available in javascript.

We make use of the OOPS concept called an inheritance to implement custom errors. All the types of standard errors like RangeError, TypeError, etc inherit a main class called Error, which looks as follows:

Syntax of Error Class Instance:

new Error(msg , filename , lineno);

where:

  • msg: The error message.
  • filename: The file where the error occurs.
  • lineno: line number of the error.

The Error class consists of properties like name, message, filename, and methods like captureStackTrace, and toString. But again it is different in different browsers.

You can use this error class to construct your own error object prototype which is called as custom error.

Custom errors can be constructed in two ways, which are:

  1. Class constructor extending error class.
  2. Function constructor inheriting error class.

Class constructor extending error class: Here we define a class that extends the error class,

Syntax:

class  CustomErrorName extends Error{
    constructor ( ){
        super( )
    }
    ...
}

Example 1: We will create an error for a condition 10 != 20 in the below code.

Javascript

class CheckCondition extends Error {

    constructor(msg) {

        super(msg);

    }

}

try {

    if (10 != 20) 

        throw new CheckCondition("10 is not equal to 20");

}

catch (err) {

    console.error(err);

}

Output:

OUTPUT

Function constructor inheriting error class:

Here the function inherits the prototype of the error class. we can then create other custom properties and methods to handle the application-specific error.

Syntax:

function CustomErrorName(msg = "") {
    this.message = msg;
    this.name = "CustomErrorName";
}
CustomErrorName.prototype = Error.prototype;

Example 2: In this example, we will create a custom error that throws for the condition 10 != 20.

Javascript

function CheckCondition(msg = "") {

    this.msg = msg;

    this.name = "CheckCondition";

}

CheckCondition.prototype = Error.prototype;

try {

    if (10 != 20) 

        throw new CheckCondition("10 is not equal to 20");

}

catch (err) {

    console.error(err);

}

Output:

OUTPUT

In this way, we can create errors specific to our applications in javascript and by making some conditions we can throw the custom errors whenever the users of the applications make a mistake in using the applications.

Last Updated :
22 Aug, 2022

Like Article

Save Article

Автор оригинала: Pankaj Kumar.

При создании приложений один из важных факторов – убедиться, что люди знают, что они получают читаемые сообщения об ошибках. Python позволяет определить пользовательские исключения. В этой статье мы обсудим, какие пользовательские исключения являются, как вы можете определить пользовательские исключения в Python, и каковы случаи использования пользовательских исключений. Давайте направимся в это!

В Учебном погрузочному учебное пособие на погрузочному обеспечению Python мы обсуждали, что такое исключение. Теперь, почему вам нужно создать пользовательское исключение, когда Python дает вам исключения и сообщения об ошибках самостоятельно? Посмотрите на пример ниже:

В поле «Сообщение» говорится, что в коде есть синтаксическая ошибка. И это такие видные сообщения об ошибках, которые Python отображается даже пользователю, когда что-то выходит не так внутри кода.

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

Как определить пользовательские исключения в Python?

Теперь есть несколько способов создавать определенные пользовательские исключения в Python, и мы пройдем некоторые общие в этой статье.

1. Использование заявления Assert

Заявление ASSERT – это условное повторное повторное использование ключевое слово, которое позволяет проверить конкретные критерии для выполнения. Если условие не встречается, то он будет бросать AssertionError.

Давайте возьмем пример здесь. Если у вас есть программа, которая запрашивает возраст пользователя. И вы хотите убедиться, что пользователи в возрасте до 18 лет не входят в систему. Вы, очевидно, можете использовать состояние IF-Evel в Python.

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

def input_age(age):
   try:
       assert int(age) > 18
   except ValueError:
       return 'ValueError: Cannot convert into int'
   else:
       return 'Age is saved successfully'

print(input_age('23'))  # This will print
print(input_age(25))  # This will print
print(input_age('nothing'))  # This will raise ValueError which is handled
print(input_age('18'))  # This will raise AssertionError and the the program collapse
print(input_age(43))  # This will not print

Выход следующей программы будет

Age is saved successfully
Age is saved successfully
ValueError: Cannot convert into int

Traceback (most recent call last):
  File "/home/imtiaz/ExceptionHandling.py", line 13, in 
    print(input_age('18'))  # This will raise AssertionError the the program collapse
  File "/home/imtiaz/ExceptionHandling.py", line 3, in input_age
    assert int(age) > 18
AssertionError

2. Повышение исключения

Вы можете поднять существующее исключение, используя Поднять ключевое слово Отказ Итак, вы просто просто напишите поднять Ключевое слово, а затем имя исключения. Если мы изменим предыдущий код, мы получаем:

def input_age(age):
   try:
       if(int(age)<=18):
           raise ZeroDivisionError
   except ValueError:
       return 'ValueError: Cannot convert into int'
   else:
       return 'Age is saved successfully'


print(input_age('23'))  # This will execute properly
print(input_age('18'))  # This will not execute properly

Выход кода будет

Age is saved successfully
Traceback (most recent call last):
  File "/home/imtiaz/ExceptionHandling.py", line 12, in 
    print(input_age('18'))  # This will not print
  File "/home/imtiaz/ExceptionHandling.py", line 4, in input_age
    raise ZeroDivisionError
ZeroDivisionError

Хотя исключение не было связано с делением ноль, все равно мы его видим. Потому что мы подняли ZerodivisionError. Мы можем поднять любую ошибку, которую вы хотите с ключевым ключевым словами, как показано выше.

3. Класс исключения Python Custom

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

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

class UnderAge(Exception):
   pass

def verify_age(age):
   if int(age) < 18:
       raise UnderAge
   else:
       print('Age: '+str(age))

# main program
verify_age(23)  # won't raise exception
verify_age(17)  # will raise exception


И выход будет

Заключение

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

Чтобы обобщить статью, вот три метода, обсуждаемые здесь:

  • Assert ключевое слово
  • Поднять ключевое слово
  • И пользовательские классы исключения

Ссылка: https://docs.cython.org/3/tuTorial/errors.html#User-defined-exceptions.

Пользовательские ошибки, расширение Error

Когда что-то разрабатываем, то нам часто необходимы собственные классы ошибок для разных вещей, которые могут пойти не так в наших задачах. Для ошибок при работе с сетью может понадобиться HttpError, для операций с базой данных DbError, для поиска — NotFoundError и т.д.

Наши ошибки должны поддерживать базовые свойства, такие как message, name и, желательно, stack. Но также они могут иметь свои собственные свойства. Например, объекты HttpError могут иметь свойство statusCode со значениями 404, 403 или 500.

JavaScript позволяет вызывать throw с любыми аргументами, то есть технически наши классы ошибок не нуждаются в наследовании от Error. Но если использовать наследование, то появляется возможность идентификации объектов ошибок посредством obj instanceof Error. Так что лучше применять наследование.

По мере роста приложения, наши собственные ошибки образуют иерархию, например, HttpTimeoutError может наследовать от HttpError и так далее.

Расширение Error

В качестве примера рассмотрим функцию readUser(json), которая должна читать данные пользователя в формате JSON.

Пример того, как может выглядеть корректный json:

let json = `{ "name": "John", "age": 30 }`;

Внутри будем использовать JSON.parse. При получении некорректного json он будет генерировать ошибку SyntaxError. Но даже если json синтаксически верен, то это не значит, что это будет корректный пользователь, верно? Могут быть пропущены необходимые данные. Например, могут отсутствовать свойства nameи age, которые являются необходимыми для наших пользователей.

Наша функция readUser(json) будет не только читать JSON-данные, но и проверять их («валидировать»). Если необходимые поля отсутствуют или данные в неверном формате, то это будет ошибкой. Но не синтаксической ошибкой SyntaxError, потому что данные синтаксически корректны. Это будет другая ошибка.

Назовём её ошибкой валидации ValidationError и создадим для неё класс. Ошибка этого вида должна содержать информацию о поле, которое является источником ошибки.

Наш класс ValidationError должен наследовать от встроенного класса Error.

Класс Error встроенный, вот его примерный код, просто чтобы мы понимали, что расширяем:

// "Псевдокод" встроенного класса Error, определённого самим JavaScript
class Error {
  constructor(message) {
    this.message = message;
    this.name = "Error"; // (разные имена для разных встроенных классов ошибок)
    this.stack = <стек вызовов>; // нестандартное свойство, но обычно поддерживается
  }
}

Теперь давайте унаследуем от него ValidationError и попробуем новый класс в действии:

*!*
class ValidationError extends Error {
*/!*
  constructor(message) {
    super(message); // (1)
    this.name = "ValidationError"; // (2)
  }
}

function test() {
  throw new ValidationError("Упс!");
}

try {
  test();
} catch(err) {
  alert(err.message); // Упс!
  alert(err.name); // ValidationError
  alert(err.stack); // список вложенных вызовов с номерами строк для каждого
}

Обратите внимание: в строке (1) вызываем родительский конструктор. JavaScript требует от нас вызова super в дочернем конструкторе, так что это обязательно. Родительский конструктор устанавливает свойство message.

Родительский конструктор также устанавливает свойство name для "Error", поэтому в строке (2) мы сбрасываем его на правильное значение.

Попробуем использовать его в readUser(json):

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

// Использование
function readUser(json) {
  let user = JSON.parse(json);

  if (!user.age) {
    throw new ValidationError("Нет поля: age");
  }
  if (!user.name) {
    throw new ValidationError("Нет поля: name");
  }

  return user;
}

// Рабочий пример с try..catch

try {
  let user = readUser('{ "age": 25 }');
} catch (err) {
  if (err instanceof ValidationError) {
*!*
    alert("Некорректные данные: " + err.message); // Некорректные данные: Нет поля: name
*/!*
  } else if (err instanceof SyntaxError) { // (*)
    alert("JSON Ошибка Синтаксиса: " + err.message);
  } else {
    throw err; // неизвестная ошибка, пробросить исключение (**)
  }
}

Блок try..catch в коде выше обрабатывает и нашу ValidationError, и встроенную SyntaxError из JSON.parse.

Обратите внимание, как мы используем instanceof для проверки конкретного типа ошибки в строке (*).

Мы можем также проверить тип, используя err.name:

// ...
// вместо (err instanceof SyntaxError)
} else if (err.name == "SyntaxError") { // (*)
// ...

Версия с instanceof гораздо лучше, потому что в будущем мы собираемся расширить ValidationError, сделав его подтипы, такие как PropertyRequiredError. И проверка instanceof продолжит работать для новых наследованных классов. Так что это на будущее.

Также важно, что если catch встречает неизвестную ошибку, то он пробрасывает её в строке (**). Блокcatch знает, только как обрабатывать ошибки валидации и синтаксические ошибки, а другие виды ошибок (из-за опечаток в коде и другие непонятные) он должен выпустить наружу.

Дальнейшее наследование

Класс ValidationError является слишком общим. Много что может пойти не так. Свойство может отсутствовать или иметь неверный формат (например, строка как значение возраста age). Поэтому для отсутствующих свойств сделаем более конкретный класс PropertyRequiredError. Он будет нести дополнительную информацию о свойстве, которое отсутствует.

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

*!*
class PropertyRequiredError extends ValidationError {
  constructor(property) {
    super("Нет свойства: " + property);
    this.name = "PropertyRequiredError";
    this.property = property;
  }
}
*/!*

// Применение
function readUser(json) {
  let user = JSON.parse(json);

  if (!user.age) {
    throw new PropertyRequiredError("age");
  }
  if (!user.name) {
    throw new PropertyRequiredError("name");
  }

  return user;
}

// Рабочий пример с try..catch

try {
  let user = readUser('{ "age": 25 }');
} catch (err) {
  if (err instanceof ValidationError) {
*!*
    alert("Неверные данные: " + err.message); // Неверные данные: Нет свойства: name
    alert(err.name); // PropertyRequiredError
    alert(err.property); // name
*/!*
  } else if (err instanceof SyntaxError) {
    alert("Ошибка синтаксиса JSON: " + err.message);
  } else {
    throw err; // неизвестная ошибка, повторно выбросит исключение
  }
}

Новый класс PropertyRequiredError очень просто использовать: необходимо указать только имя свойства new PropertyRequiredError(property). Сообщение для пользователя message генерируется конструктором.

Обратите внимание, что свойство this.name в конструкторе PropertyRequiredError снова присвоено вручную. Правда, немного утомительно — присваивать this.name = <class name> в каждом классе пользовательской ошибки. Можно этого избежать, если сделать наш собственный «базовый» класс ошибки, который будет ставить this.name = this.constructor.name. И затем наследовать все ошибки уже от него.

Давайте назовём его MyError.

Вот упрощённый код с MyError и другими пользовательскими классами ошибок:

class MyError extends Error {
  constructor(message) {
    super(message);
*!*
    this.name = this.constructor.name;
*/!*
  }
}

class ValidationError extends MyError { }

class PropertyRequiredError extends ValidationError {
  constructor(property) {
    super("Нет свойства: " + property);
    this.property = property;
  }
}

// name корректное
alert( new PropertyRequiredError("field").name ); // PropertyRequiredError

Теперь пользовательские ошибки стали намного короче, особенно ValidationError,
так как мы избавились от строки "this.name = ..." в конструкторе.

Обёртывание исключений

Назначение функции readUser в приведённом выше коде — это «чтение данных пользователя». В процессе могут возникнуть различные виды ошибок. Сейчас у нас есть SyntaxError и ValidationError, но в будущем функция readUser может расшириться и, возможно, генерировать другие виды ошибок.

Код, который вызывает readUser, должен обрабатывать эти ошибки.

Сейчас в нём используются проверки if в блоке catch, которые проверяют класс и обрабатывают известные ошибки и пробрасывают дальше неизвестные. Но если функция readUser генерирует несколько видов ошибок, то мы должны спросить себя: действительно ли мы хотим проверять все типы ошибок поодиночке во всех местах в коде, где вызывается readUser?

Часто ответ «Нет»: внешний код хочет быть на один уровень выше всего этого. Он хочет иметь какую-то обобщённую ошибку чтения данных. Почему именно это произошло — часто не имеет значения (об этом говорится в сообщении об ошибке). Или даже лучше, если есть способ получить подробности об ошибке, но только если нам это нужно.

Итак, давайте создадим новый класс ReadError для представления таких ошибок. Если ошибка возникает внутри readUser, мы её перехватим и сгенерируем ReadError. Мы также сохраним ссылку на исходную ошибку в свойстве cause. Тогда внешний код должен будет только проверить наличие ReadError.

Этот код определяет ошибку ReadError и демонстрирует её использование в readUserи try..catch:

class ReadError extends Error {
  constructor(message, cause) {
    super(message);
    this.cause = cause;
    this.name = 'ReadError';
  }
}

class ValidationError extends Error { /*...*/ }
class PropertyRequiredError extends ValidationError { /* ... */ }

function validateUser(user) {
  if (!user.age) {
    throw new PropertyRequiredError("age");
  }

  if (!user.name) {
    throw new PropertyRequiredError("name");
  }
}

function readUser(json) {
  let user;

  try {
    user = JSON.parse(json);
  } catch (err) {
*!*
    if (err instanceof SyntaxError) {
      throw new ReadError("Синтаксическая ошибка", err);
    } else {
      throw err;
    }
*/!*
  }

  try {
    validateUser(user);
  } catch (err) {
*!*
    if (err instanceof ValidationError) {
      throw new ReadError("Ошибка валидации", err);
    } else {
      throw err;
    }
*/!*
  }

}

try {
  readUser('{bad json}');
} catch (e) {
  if (e instanceof ReadError) {
*!*
    alert(e);
    // Исходная ошибка: SyntaxError:Unexpected token b in JSON at position 1
    alert("Исходная ошибка: " + e.cause);
*/!*
  } else {
    throw e;
  }
}

В приведённом выше коде readUser работает так, как описано — функция распознаёт синтаксические ошибки и ошибки валидации и выдаёт вместо них ошибки ReadError (неизвестные ошибки, как обычно, пробрасываются).

Внешний код проверяет только instanceof ReadError. Не нужно перечислять все возможные типы ошибок

Этот подход называется «обёртывание исключений», потому что мы берём «исключения низкого уровня» и «оборачиваем» их в ReadError, который является более абстрактным и более удобным для использования в вызывающем коде. Такой подход широко используется в объектно-ориентированном программировании.

Итого

  • Мы можем наследовать свои классы ошибок от Error и других встроенных классов ошибок, но нужно позаботиться о свойстве name и не забыть вызвать super.
  • Мы можем использовать instanceof для проверки типа ошибок. Это также работает с наследованием. Но иногда у нас объект ошибки, возникшей в сторонней библиотеке, и нет простого способа получить класс. Тогда для проверки типа ошибки можно использовать свойство name.
  • Обёртывание исключений является распространённой техникой: функция ловит низкоуровневые исключения и создаёт одно «высокоуровневое» исключение вместо разных низкоуровневых. Иногда низкоуровневые исключения становятся свойствами этого объекта, как err.cause в примерах выше, но это не обязательно.

«What is the proper way to declare custom exceptions in modern Python?»

This is fine unless your exception is really a type of a more specific exception:

class MyException(Exception):
    pass

Or better (maybe perfect), instead of pass give a docstring:

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

Subclassing Exception Subclasses

From the docs

Exception

All built-in, non-system-exiting exceptions are derived from this class.
All user-defined exceptions should also be derived from this
class.

That means that if your exception is a type of a more specific exception, subclass that exception instead of the generic Exception (and the result will be that you still derive from Exception as the docs recommend). Also, you can at least provide a docstring (and not be forced to use the pass keyword):

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

Set attributes you create yourself with a custom __init__. Avoid passing a dict as a positional argument, future users of your code will thank you. If you use the deprecated message attribute, assigning it yourself will avoid a 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) 

There’s really no need to write your own __str__ or __repr__. The built-in ones are very nice, and your cooperative inheritance ensures that you use them.

Critique of the top answer

Maybe I missed the question, but why not:

class MyException(Exception):
    pass

Again, the problem with the above is that in order to catch it, you’ll either have to name it specifically (importing it if created elsewhere) or catch Exception, (but you’re probably not prepared to handle all types of Exceptions, and you should only catch exceptions you are prepared to handle). Similar criticism to the below, but additionally that’s not the way to initialize via super, and you’ll get a DeprecationWarning if you access the message attribute:

Edit: to override something (or pass extra args), do this:

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

That way you could pass dict of error messages to the second param, and get to it later with e.errors

It also requires exactly two arguments to be passed in (aside from the self.) No more, no less. That’s an interesting constraint that future users may not appreciate.

To be direct — it violates Liskov substitutability.

I’ll demonstrate both errors:

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

Compared to:

>>> MyAppValueError('foo', 'FOO', 'bar').message
'foo'
  1. Создайте собственный класс исключений в Python
  2. Выполнение обработки исключений с помощью блока try...except в Python

Создание настраиваемых исключений в Python

Это руководство продемонстрирует, как вы можете создавать собственные классы исключений в Python. Здесь мы покажем, как правильно выполнять обработку исключений, определять собственные классы исключений и переопределять существующие встроенные исключения.

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

Создание класса исключения в Python выполняется так же, как и для обычного класса. Основное отличие состоит в том, что вы должны включить базовый класс Python Exception, чтобы сообщить компилятору, что создаваемый вами класс является классом исключения.

Давайте протестируем этот метод, чтобы создать класс исключения с именем DemoException и использовать ключевое слово потока управления заполнителем pass внутри в качестве заполнителя.

class DemoException(Exception):
    pass

Выполнение вызова исключения с использованием ключевого слова raise в Python

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

Используя ключевое слово raise, запускает исключение, используя данный класс исключения, и выводит сообщение об исключении.

class DemoException(Exception):
    pass

raise DemoException

Выход:

Traceback (most recent call last):
  File "/Users/demo/python/demo_exception.py", line 4, in <module>
    raise DemoException
__main__.DemoException

Стандартное исключение будет выглядеть в терминале, если не было объявлено настраиваемое сообщение об исключении.

Объявление настраиваемого сообщения об исключении в Python

Чтобы объявить настраиваемое сообщение об исключении для DemoException, переопределите метод __init__() класса исключения и включите в параметры сообщение, которое должно выводиться для исключения, вместе с обязательным параметром ссылки на себя self.

Например, переопределим метод __init__() и создадим собственное сообщение для класса DemoException:

class DemoException(Exception):
    def __init__(self, message):
        super().__init__(message)

Обратите внимание, что для успешной интеграции сообщения в ваше исключение вызовите базовый класс Exception, метод __init__() и включите message в качестве аргумента.

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

class DemoException(Exception):
    def __init__(self, message):
        super().__init__(message)
        
message = "Exception Triggered! Something went wrong."
raise DemoException(message)

Результат должен выглядеть так:

Traceback (most recent call last):
  File "/Users/demo/python/helloworld.py", line 6, in <module>
    raise DemoException(message)
__main__.DemoException: Exception Triggered! Something went wrong.

Теперь мы успешно создали и запустили класс исключения с настраиваемым сообщением об ошибке.

В реальных ситуациях, которые могут вызвать исключение, как мы обрабатываем и вызываем эти исключения? Вы можете аккуратно решить эту проблему, реализовав обработку исключений с помощью блока try...except.

Выполнение обработки исключений с помощью блока try...except в Python

Блок try...except очень похож на блок try-catch в других языках, таких как Java.

Блок try...except имеет 2 основных блока и 2 дополнительных блока:

  • try (обязательно) — основной блок, отвечающий за инкапсуляцию блока кода, в котором может быть вызвано исключение. Блок try останавливает весь процесс в нем при возникновении исключения.
  • except (обязательно) — программа блока продолжается всякий раз, когда инициируется указанное исключение. Этот блок обычно содержит описательное сообщение об ошибке для вызывающего абонента или просто простую инструкцию print(). В одном блоке try может быть более одного блока except, каждый из которых перехватывает разные исключения.
  • else (необязательно) — в этом необязательном блоке программа продолжит работу, если блок try не вызвал никаких исключений.
  • finally (необязательно) — этот необязательный блок запускается после того, как все из предыдущих 3 блоков было выполнено, независимо от того, инициировано ли исключение или нет.

Давайте воспользуемся предыдущим примером с использованием класса DemoException, чтобы попробовать простой блок try...except.

Сначала оберните ключевое слово raise в функцию и поместите его в блок try...except.

Функция, которую мы создадим для этого примера, — это функция, которая принимает число и выдает исключение, если оно отправляет 0. Если он отправит любой другой номер, то код будет работать так, как задумано. Посмотрите пример ниже:

class DemoException(Exception):
    def __init__(self, message):
        super().__init__(message)
        

message = "Exception Triggered! Something went wrong."

def triggerException(num):
    if (num == 0):
        raise DemoException(message)
    else:
        print(num)


try:
    triggerException(0)
    print("Code has successfully been executed.")
except DemoException:
    print("Error: Number should not be 0.")

Поскольку triggerException() передало 0 в качестве аргумента, код должен вызвать DemoException. Здесь мы должны ожидать, что сообщение с ключевым словом raise будет заменено тем, что находится внутри блока except в качестве вывода.

Обратите внимание, что строка print() после вызова функции triggerException() не выводилась. Это потому, что функция вызвала исключение; поэтому он немедленно остановил все процессы в блоке try и перешел непосредственно к блоку except.

Выход:

Error: Number should not be 0.

Теперь давайте попробуем передать допустимое число, например, 20.

try:
    triggerException(20)
    print("Code has successfully been executed.")
except DemoException:
    print("Error: Number should not be 0.")

Выход:

20
Code has successfully been executed.

Попробуем объединить блоки except и создать еще одно исключение. Назовем новое исключение NumberFormatException, которое срабатывает, если заданный вход не является числом. Для этого класса исключения давайте объявим сообщение внутри класса.

class NumberFormatException(Exception, value):
    message = f'{value} is not a number'
    def __init__(self):
        super().__init__(message)

Теперь измените приведенный выше код для обработки нового класса исключения NumberFormatException:

class DemoException(Exception):
    def __init__(self, message):
        super().__init__(message)
        
class NumberFormatException(Exception):
    def __init__(self, message, value):
        message = f'{value} is not a number'
        super().__init__(message)
        
message = "Exception occured."

def triggerException(num):
    if (not num.isdigit()):
        raise NumberFormatException(message, num)
    elif (num == 0):
        raise DemoException(message)
    else:
        print(num)

num = "sample string"
try:
    triggerException(num)
    print("Code has successfully been executed.")
except DemoException:
    print("Error: Number should not be 0.")
except NumberFormatException:
    print(num+" is not a number.")

В этом коде значение num, которое было передано в triggerException(), является строкой 'sample string', поэтому должно сработать NumberFormatException.

Выход:

sample string is not a number.

Таким образом, создание пользовательских исключений в Python так же просто, как создание нового класса, но с классом Exception в качестве дополнительного аргумента в определении класса. Ключевое слово raise используется для запуска исключений с учетом класса исключения. Блоки try...except используются для обертывания одного или нескольких исключений в блоке кода и изменения того, что делает код при обработке этого исключения, а не просто для полного завершения программы.

В Python пользователи могут определять свои собственные исключения, создавая новый класс. Этот класс исключений должен прямо или косвенно быть производным от встроенного класса Exception. Большинство встроенных исключений также являются производными от этого класса. Все пользовательские исключения также должны быть производными от этого класса.

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

В примере создается определяемое пользователем исключение CustomError(), которое наследуется от класса Exception. Это новое исключение, как и другие исключения, может быть вызвано с помощью оператора raise с дополнительным сообщением об ошибке.

# Определяем собственное исключение
>>> class CustomError(Exception):
...     pass
...
>>> raise CustomError
# Traceback (most recent call last):
# ...
# __main__.CustomError

# Вызываем собственное исключение 
# 'CustomError' с сообщением об ошибке
>>> raise CustomError("An error occurred")
# Traceback (most recent call last):
# ...
# __main__.CustomError: An error occurred

При разработке программы на Python, хорошей практикой считается помещать все определяемые пользователем исключения в отдельный файл. Многие стандартные модули определяют свои исключения отдельно как exceptions.py или errors.py (обычно, но не всегда).

Пользовательский класс исключений может реализовать все, что может делать обычный класс, но обычно их делают простыми и краткими. Большинство реализаций пользовательских исключений объявляют настраиваемый базовый класс и наследуют другие классы исключений из этого базового класса.

Большинство пользовательских исключений определяются именами, которые заканчиваются на «Error», аналогично именованию стандартных исключений.

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

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

# определение пользовательских исключений
class Error(Exception):
    """Базовый класс для других исключений"""
    pass

class ValueTooSmallError(Error):
    """Вызывается, когда входное значение мало"""
    pass

class ValueTooLargeError(Error):
    """Вызывается, когда входное значение велико"""
    pass

# число, которое нужно угадать
number = 10

# игра продолжается до тех пор, 
# пока пользователь его не угадает
while True:
    try:
        i_num = int(input("Ввести число: "))
        if i_num < number:
            raise ValueTooSmallError
        elif i_num > number:
            raise ValueTooLargeError
        break
    except ValueTooSmallError:
        print("Это число меньше загаданного, попробуйте еще раз!n")
    except ValueTooLargeError:
        print("Это число больше загаданного, попробуйте еще раз!n")

print("Поздравляю! Вы правильно угадали.")

В примере определен базовый класс под названием Error(). Два других исключения, которые фактически вызываются программой (ValueTooSmallError и ValueTooLargeError), являются производными от класса Error().

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

Пример запуска скрипта с примером:

Ввести число: 12
Это число больше загаданного, попробуйте еще раз!

Ввести число: 0
Это число меньше загаданного, попробуйте еще раз!

Ввести число: 8
Это число меньше загаданного, попробуйте еще раз!

Ввести число: 10
Поздравляю! Вы правильно угадали.

Смотрим еще один простенький пример.

Пользовательские классы исключений, часто предлагая только ряд атрибутов, которые позволяют извлекать информацию об ошибке для исключения.

class Error(Exception):
    """Базовый класс для исключений в этом модуле."""
    pass

class InputError(Error):
    """Исключение для ошибок во входных данных.
    Attributes:
        expression -- выражение, в котором произошла ошибка
        message -- объяснение ошибки
    """
    def __init__(self, expression, message):
        self.expression = expression
        self.message = message


x = input("Ведите положительное целое число: ")
try:
    x = int(x)
    if x < 0:
        raise InputError(f'!!! x = input({x})', 
                         '-> Допустимы только положительные числа.')
except ValueError:
    print("Error type of value!")
except InputError as e:
    print(e.args[0])
    print(e.args[1])
else:
    print(x)


# Ведите положительное целое число: 3
# 3

# Ведите положительное целое число: 7.9
# Error type of value!

# Ведите положительное целое число: -5
# !!! x = input(-5)
# -> Допустимы только положительные числа.

У объектов класса исключений Exception и его производных, определен метод __str__() так, чтобы выводить значения атрибутов. Поэтому можно не обращаться напрямую к полям объекта: e.expression и e.message. Кроме того у экземпляров класса исключений Exception есть атрибут args. Через него можно получать доступ к отдельным полям, как показано в примере выше.

Многие стандартные модули определяют свои собственные исключения для сообщений об ошибках, которые могут возникать в определяемых ими функциях. Более подробная информация о классах представлена в разделе «Классы в Python».

Настройка собственных классов исключений.

Для тонкой настройки своего класса исключения нужно иметь базовые знания объектно-ориентированного программирования.

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

class SalaryNotInRangeError(Exception):
    """Исключение возникает из-за ошибок в зарплате.

    Атрибуты:
        salary: входная зарплата, вызвавшая ошибку
        message: объяснение ошибки
    """

    def __init__(self, salary, message="Зарплата не входит в диапазон (5000, 15000)"):
        self.salary = salary
        self.message = message
        # переопределяется конструктор встроенного класса `Exception()`
        super().__init__(self.message)


salary = int(input("Введите сумму зарплаты: "))
if not 5000 < salary < 15000:
    raise SalaryNotInRangeError(salary)

В примере, для приема аргументов salary и message переопределяется конструктор встроенного класса Exception(). Затем конструктор родительского класса Exception() вызывается вручную с аргументом self.message при помощи функции super(). Пользовательский атрибут self.salary определен для использования позже.

Результаты запуска скрипта:

Введите сумму зарплаты: 2000
Traceback (most recent call last):
  File "test.py", line 17, in <module>
    raise SalaryNotInRangeError(salary)
__main__.SalaryNotInRangeError: Зарплата не входит в диапазон (5000, 15000)

Унаследованный метод __str__ класса Exception() используется для отображения соответствующего сообщения при возникновении SalaryNotInRangeError(). Также можно настроить сам метод __str__, переопределив его.

class SalaryNotInRangeError(Exception):
    """Исключение возникает из-за ошибок в зарплате.

    Атрибуты:
        salary: входная зарплата, вызвавшая ошибку
        message: объяснение ошибки
    """

    def __init__(self, salary, message="Зарплата не входит в диапазон (5000, 15000)"):
        self.salary = salary
        self.message = message
        super().__init__(self.message)

    # переопределяем метод '__str__'
    def __str__(self):
        return f'{self.salary} -> {self.message}'

salary = int(input("Введите сумму зарплаты: "))
if not 5000 < salary < 15000:
    raise SalaryNotInRangeError(salary)

Вывод работы скрипта:

Введите сумму зарплаты: 2000
Traceback (most recent call last):
  File "test.py", line 20, in <module>
    raise SalaryNotInRangeError(salary)
__main__.SalaryNotInRangeError: 2000 -> Зарплата не входит в диапазон (5000, 15000)

Как перехватывать пользовательское исключение.

Если необходимо, чтобы код использовал пользовательское исключение, то сначала нужно перехватить исключение, определяемое используемым модулем, а затем повторно вызвать, при помощи raise, своё собственное исключение.

import sqlite3 

class MyError(Exception):
     """Could not connect to db"""
     pass

try:         
    conn= sqlite3.connect('database.sqlite')
except sqlite3.Error as e:
    raise MyError(f'Could not connect to db: {e.value}')

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

An Exception is raised whenever there is an error encountered, and it signifies that something went wrong with the program. By default, there are many exceptions that the language defines for us, such as TypeError when the wrong type is passed. In this article, we shall look at how we can create our own Custom Exceptions in Python.

But before we take a look at how custom exceptions are implemented, let us find out how we could raise different types of exceptions in Python.


Raise Exceptions

Python allows the programmer to raise an Exception manually using the raise keyword.

Format: raise ExceptionName

The below function raises different exceptions depending on the input passed to the function.

def exception_raiser(string):
    if isinstance(string, int):
        raise ValueError
    elif isinstance(string, str):
        raise IndexError
    else:
        raise TypeError

Output:

>>> exception_raiser(123)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in exception_raiser
ValueError
>>> exception_raiser('abc')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in exception_raiser
IndexError
>>> exception_raiser([123, 456])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in exception_raiser
TypeError

As you can observe, different types of Exceptions are raised based on the input, at the programmer’s choice. This allows for good flexibility of Error Handling as well, since we can actively predict why an Exception can be raised.


Defining Custom Exceptions

Similarly, Python also allows us to define our own custom Exceptions. We are in complete control of what this Exception can do, and when it can be raised, using the raise keyword. Let us look at how we can define and implement some custom Exceptions.

1. Create a Custom Exception Class

We can create a custom Exception class to define the new Exception. Again, the idea behind using a Class is because Python treats everything as a Class. So it doesn’t seem that outlandish that an Exception can be a class as well!

All Exceptions inherit the parent Exception Class, which we shall also inherit when creating our class.

We shall create a Class called MyException, which raises an Exception only if the input passed to it is a list and the number of elements in the list is odd.

class MyException(Exception):
	pass

def list_check(lst):
    if len(lst) % 2 != 0:
        raise MyException

# MyException will not be raised
list_check([1, 2, 3, 4])

# MyException will be raised
list_check([1, 3, 5])    

Output:

[email protected]:~# python3 exceptions.py
Traceback (most recent call last):
  File "exceptions.py", line 12, in <module>
    list_check([1, 3, 5])
  File "exceptions.py", line 6, in list_check
    raise MyException
__main__.MyException

2. Add a custom Message and Error

We can add our own error messages and print them to the console for our Custom Exception. This involves passing two other parameters in our MyException class, the message and error parameters.

Let us modify our original code to account for a custom Message and Error for our Exception.

class MyException(Exception):
    def __init__(self, message, errors):
        # Call Exception.__init__(message)
        # to use the same Message header as the parent class
        super().__init__(message)
        self.errors = errors
        # Display the errors
        print('Printing Errors:')
        print(errors)

def list_check(lst):
    if len(lst) % 2 != 0:
        raise MyException('Custom Message', 'Custom Error')

# MyException will not be raised
list_check([1, 2, 3, 4])

# MyException will be raised
list_check([1, 3, 5])

Output:

Printing Errors:
Custom Error
Traceback (most recent call last):
  File "exceptions.py", line 17, in <module>
    list_check([1, 3, 5])
  File "exceptions.py", line 11, in list_check
    raise MyException('Custom Message', 'Custom Error')
__main__.MyException: Custom Message

We have thus successfully implemented our own Custom Exceptions, including adding custom error messages for debugging purposes! This can be very useful if you are building a Library/API and another programmer wants to know what exactly went wrong when the custom Exception is raised.


Conclusion

In this article, we learned how to raise Exceptions using the raise keyword, and also build our own Exceptions using a Class and add error messages to our Exception.

References

  • JournalDev article on Custom Exceptions
  • Exception Handling in Python

«What is the proper way to declare custom exceptions in modern Python?»

This is fine unless your exception is really a type of a more specific exception:

class MyException(Exception):
    pass

Or better (maybe perfect), instead of pass give a docstring:

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

Subclassing Exception Subclasses

From the docs

Exception

All built-in, non-system-exiting exceptions are derived from this class.
All user-defined exceptions should also be derived from this
class.

That means that if your exception is a type of a more specific exception, subclass that exception instead of the generic Exception (and the result will be that you still derive from Exception as the docs recommend). Also, you can at least provide a docstring (and not be forced to use the pass keyword):

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

Set attributes you create yourself with a custom __init__. Avoid passing a dict as a positional argument, future users of your code will thank you. If you use the deprecated message attribute, assigning it yourself will avoid a 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) 

There’s really no need to write your own __str__ or __repr__. The built-in ones are very nice, and your cooperative inheritance ensures that you use them.

Critique of the top answer

Maybe I missed the question, but why not:

class MyException(Exception):
    pass

Again, the problem with the above is that in order to catch it, you’ll either have to name it specifically (importing it if created elsewhere) or catch Exception, (but you’re probably not prepared to handle all types of Exceptions, and you should only catch exceptions you are prepared to handle). Similar criticism to the below, but additionally that’s not the way to initialize via super, and you’ll get a DeprecationWarning if you access the message attribute:

Edit: to override something (or pass extra args), do this:

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

That way you could pass dict of error messages to the second param, and get to it later with e.errors

It also requires exactly two arguments to be passed in (aside from the self.) No more, no less. That’s an interesting constraint that future users may not appreciate.

To be direct — it violates Liskov substitutability.

I’ll demonstrate both errors:

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

Compared to:

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

«What is the proper way to declare custom exceptions in modern Python?»

This is fine unless your exception is really a type of a more specific exception:

class MyException(Exception):
    pass

Or better (maybe perfect), instead of pass give a docstring:

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

Subclassing Exception Subclasses

From the docs

Exception

All built-in, non-system-exiting exceptions are derived from this class.
All user-defined exceptions should also be derived from this
class.

That means that if your exception is a type of a more specific exception, subclass that exception instead of the generic Exception (and the result will be that you still derive from Exception as the docs recommend). Also, you can at least provide a docstring (and not be forced to use the pass keyword):

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

Set attributes you create yourself with a custom __init__. Avoid passing a dict as a positional argument, future users of your code will thank you. If you use the deprecated message attribute, assigning it yourself will avoid a 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) 

There’s really no need to write your own __str__ or __repr__. The built-in ones are very nice, and your cooperative inheritance ensures that you use them.

Critique of the top answer

Maybe I missed the question, but why not:

class MyException(Exception):
    pass

Again, the problem with the above is that in order to catch it, you’ll either have to name it specifically (importing it if created elsewhere) or catch Exception, (but you’re probably not prepared to handle all types of Exceptions, and you should only catch exceptions you are prepared to handle). Similar criticism to the below, but additionally that’s not the way to initialize via super, and you’ll get a DeprecationWarning if you access the message attribute:

Edit: to override something (or pass extra args), do this:

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

That way you could pass dict of error messages to the second param, and get to it later with e.errors

It also requires exactly two arguments to be passed in (aside from the self.) No more, no less. That’s an interesting constraint that future users may not appreciate.

To be direct — it violates Liskov substitutability.

I’ll demonstrate both errors:

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

Compared to:

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

Summary: in this tutorial, you’ll learn how to define Python custom exception classes.

Introduction to the Python custom exception

To create a custom exception class, you define a class that inherits from the built-in Exception class or one of its subclasses such as ValueError class:

The following example defines a CustomException class that inherits from the Exception class:

class CustomException(Exception): """ my custom exception class """

Code language: Python (python)

Note that the CustomException class has a docstring that behaves like a statement. Therefore, you don’t need to add the pass statement to make the syntax valid.

To raise the CustomException, you use the raise statement. For example, the following uses the raise statement to raise the CustomException:

class CustomException(Exception): """ my custom exception class """ try: raise CustomException('This is my custom exception') except CustomException as ex: print(ex)

Code language: Python (python)

Output:

This is my custom exception

Code language: Python (python)

Like standard exception classes, custom exceptions are also classes. Hence, you can add functionality to the custom exception classes like:

  • Adding attributes and properties.
  • Adding methods e.g., log the exception, format the output, etc.
  • Overriding the __str__ and __repr__ methods
  • And doing anything else that you can do with regular classes.

In practice, you’ll want to keep the custom exceptions organized by creating a custom exception hierarchy. The custom exception hierarchy allows you to catch exceptions at multiple levels, like the standard exception classes.

Suppose you need to develop a program that converts a temperature from Fahrenheit to Celsius.

The minimum and maximum values of a temperature in Fahrenheit are 32 and 212. If users enter a value that is not in this range, you want to raise a custom exception e.g., FahrenheitError.

Define the FahrenheitError custom exception class

The following defines the FahrenheitError exception class:

class FahrenheitError(Exception): min_f = 32 max_f = 212 def __init__(self, f, *args): super().__init__(args) self.f = f def __str__(self): return f'The {self.f} is not in a valid range {self.min_f, self.max_f}'

Code language: Python (python)

How it works.

  • First, define the FahrenheitError class that inherits from the Exception class.
  • Second, add two class attributes min_f and max_f that represent the minimum and maximum Fahrenheit values.
  • Third, define the __init__ method that accepts a Fahrenheit value (f) and a number of position arguments (*args). In the __init__ method, call the __init__ method of the base class. Also, assign the f argument to the f instance attribute.
  • Finally, override the __str__ method to return a custom string representation of the class instance.

Define the fahrenheit_to_celsius function

The following defines the fahrenheit_to_celsius function that accepts a temperature in Fahrenheit and returns a temperature in Celcius:

def fahrenheit_to_celsius(f: float) -> float: if f < FahrenheitError.min_f or f > FahrenheitError.max_f: raise FahrenheitError(f) return (f - 32) * 5 / 9

Code language: Python (python)

The fahrenheit_to_celsius function raises the FahrenheitError excpetion if the input temperature is not in the valid range. Otherwise, it converts the temperature from Fahrenheit to Celcius.

Create the main program

The following main program uses the fahrenheit_to_celsius function and the FahrenheitError custom exception class:

if __name__ == '__main__': f = input('Enter a temperature in Fahrenheit:') try: f = float(f) except ValueError as ex: print(ex) else: try: c = fahrenheit_to_celsius(float(f)) except FahrenheitError as ex: print(ex) else: print(f'{f} Fahrenheit = {c:.4f} Celsius')

Code language: Python (python)

How it works.

First, prompt users for a temperature in Fahrenheit.

f = input('Enter a temperature in Fahrenheit:')

Code language: Python (python)

Second, convert the input value into a float. If the float() cannot convert the input value, the program will raise a ValueError exception. In this case, it displays the error message from the ValueError exception:

try: f = float(f) # ... except ValueError as ex: print(ex)

Code language: Python (python)

Third, convert the temperature to Celsius by calling the fahrenheit_to_celsius function and print the error message if the input value is not a valid Fahrenheit value:

try: c = fahrenheit_to_celsius(float(f)) except FahrenheitError as ex: print(ex) else: print(f'{f} Fahrenheit = {c:.4f} Celsius')

Code language: Python (python)

Put it all together

class FahrenheitError(Exception): min_f = 32 max_f = 212 def __init__(self, f, *args): super().__init__(args) self.f = f def __str__(self): return f'The {self.f} is not in a valid range {self.min_f, self.max_f}' def fahrenheit_to_celsius(f: float) -> float: if f < FahrenheitError.min_f or f > FahrenheitError.max_f: raise FahrenheitError(f) return (f - 32) * 5 / 9 if __name__ == '__main__': f = input('Enter a temperature in Fahrenheit:') try: f = float(f) except ValueError as ex: print(ex) else: try: c = fahrenheit_to_celsius(float(f)) except FahrenheitError as ex: print(ex) else: print(f'{f} Fahrenheit = {c:.4f} Celsius')

Code language: Python (python)

Summary

  • Subclass the Exception class or one of its subclasses to define a custom exception class.
  • Create a exception class hierarchy to make the exception classes more organized and catch exceptions at multiple levels.

Did you find this tutorial helpful ?

In the previous tutorial, we learned about different built-in exceptions in Python and why it is important to handle exceptions. .

However, sometimes we may need to create our own custom exceptions that serve our purpose.


Defining Custom Exceptions

In Python, we can define custom exceptions by creating a new class that is derived from the built-in Exception class.

Here’s the syntax to define custom exceptions,

class CustomError(Exception):
    ...
    pass

try:
   ...

except CustomError:
    ...

Here, CustomError is a user-defined error which inherits from the Exception class.

Note:

  • When we are developing a large Python program, it is a good practice to place all the user-defined exceptions that our program raises in a separate file.
  • Many standard modules define their exceptions separately as exceptions.py or errors.py (generally but not always).

Example: Python User-Defined Exception

# define Python user-defined exceptions
class InvalidAgeException(Exception):
    "Raised when the input value is less than 18"
    pass

# you need to guess this number
number = 18

try:
    input_num = int(input("Enter a number: "))
    if input_num < number:
        raise InvalidAgeException
    else:
        print("Eligible to Vote")
        
except InvalidAgeException:
    print("Exception occurred: Invalid Age")

Output

If the user input input_num is greater than 18,

Enter a number: 45
Eligible to Vote

If the user input input_num is smaller than 18,

Enter a number: 14
Exception occurred: Invalid Age

In the above example, we have defined the custom exception InvalidAgeException by creating a new class that is derived from the built-in Exception class.

Here, when input_num is smaller than 18, this code generates an exception.

When an exception occurs, the rest of the code inside the try block is skipped.

The except block catches the user-defined InvalidAgeException exception and statements inside the except block are executed.


Customizing Exception Classes

We can further customize this class to accept other arguments as per our needs.

To learn about customizing the Exception classes, you need to have the basic knowledge of Object-Oriented programming.

Visit Python Object Oriented Programming to learn about Object-Oriented programming in Python.

Let’s see an example,

class SalaryNotInRangeError(Exception):
    """Exception raised for errors in the input salary.

    Attributes:
        salary -- input salary which caused the error
        message -- explanation of the error
    """

    def __init__(self, salary, message="Salary is not in (5000, 15000) range"):
        self.salary = salary
        self.message = message
        super().__init__(self.message)


salary = int(input("Enter salary amount: "))
if not 5000 < salary < 15000:
    raise SalaryNotInRangeError(salary)

Output

Enter salary amount: 2000
Traceback (most recent call last):
  File "<string>", line 17, in <module>
    raise SalaryNotInRangeError(salary)
__main__.SalaryNotInRangeError: Salary is not in (5000, 15000) range

Here, we have overridden the constructor of the Exception class to accept our own custom arguments salary and message.

Then, the constructor of the parent Exception class is called manually with the self.message argument using super().

The custom self.salary attribute is defined to be used later.

The inherited __str__ method of the Exception class is then used to display the corresponding message when SalaryNotInRangeError is raised.

Понравилась статья? Поделить с друзьями:
  • Что такое кармические ошибки
  • Что такое кадастровая ошибка определение
  • Что такое кадастровая ошибка и как ее исправить
  • Что такое исправление реестровой ошибки
  • Что такое исправление кадастровой ошибки