[Перевод] Стоит ли использовать кастомные исключения в Python

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

18f48d5fe28738444e7be6cadfc5cb2f.png

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

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

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

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

Авторы книги «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 
    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 
    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»: «Простое лучше, чем сложное», «Плоское лучше, чем вложенное»

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

© Habrahabr.ru