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. Коли перехоплювати, а коли поширювати

Корисне правило:

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

  • Дозволяйте поширюватись, коли немає нічого корисного, крім аварійного завершення, або коли вищий рівень – це той, хто знає, як відновитись.

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