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. Когда перехватывать, а когда распространять дальше#
Полезное эмпирическое правило:
Перехватывайте исключение на том уровне, который может осмысленно восстановиться – подставить значение по умолчанию, повторить попытку, пропустить некорректный ввод.
Позвольте ему распространяться дальше, когда не остаётся ничего полезного, кроме аварийного завершения, или когда именно вышестоящий слой знает, как восстановиться.
Функция в середине стека вызовов, которая проглатывает каждую ошибку и тихо возвращается, делает сбои неотслеживаемыми. Предпочитайте позволять исключениям путешествовать, пока они не достигнут кода, у которого действительно есть план.