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. Когда перехватывать, а когда распространять дальше#

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

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

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

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