2.26. Возбуждение ошибок

Функция может сигнализировать своему вызывающему коду о проблеме, возбуждая исключение. Ключевое слово – raise:

def square_root(x):
    if x < 0:
        raise ValueError("square_root expects a non-negative number")
    return x ** 0.5

Вызов square_root(-1) останавливается на строке raise, выходит из функции и ищет подходящий except в вызывающем коде. Если ни один вызывающий код не перехватывает его, скрипт завершается с трассировкой.

2.26.1. Почему возбуждать исключение, а не возвращать сигнальное значение

Два способа сообщить о «некорректном вводе»:

# signal with a sentinel
def square_root_or_none(x):
    if x < 0:
        return None
    return x ** 0.5

# raise an exception
def square_root(x):
    if x < 0:
        raise ValueError("...")
    return x ** 0.5

Форма с исключением обычно лучше:

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

  • Сообщение об ошибке путешествует вместе с исключением; подход с сигнальными значениями вынужден прикреплять диагностику где-то ещё.

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

Используйте сигнальные значения только тогда, когда «не найдено» – это рутинный, неисключительный исход – dict.get() возвращает None при отсутствующем ключе именно потому, что ожидается, что поиск иногда будет неудачным.

2.26.2. Пользовательские классы исключений

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

class ConfigError(Exception):
    pass

def load_config(path):
    try:
        f = open(path)
    except OSError as e:
        raise ConfigError("missing config file: " + path)

try:
    load_config("settings.json")
except ConfigError as e:
    print("startup failed:", e)

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

2.26.2.1. Повторное возбуждение

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

try:
    do_work()
except Exception as e:
    log(e)
    raise        # let it keep going

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

2.26.3. Когда перехватывать, а когда распространять дальше

Полезное эмпирическое правило:

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

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

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