Коли і як слід використовувати винятки?


20

Установка

У мене часто виникають проблеми з визначенням, коли і як використовувати винятки. Розглянемо простий приклад: припустимо, я перебираю веб-сторінку, скажімо, " http://www.abevigoda.com/ ", щоб визначити, чи Abe Vigoda ще живий. Для цього все, що нам потрібно зробити, - це завантажити сторінку та шукати рази, коли з’явиться фраза «Abe Vigoda». Ми повертаємо першу появу, оскільки вона включає статус Абе. Концептуально це буде виглядати приблизно так:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Де parse_abe_status(s)бере рядок форми "Abe Vigoda - це щось " і повертає частину " щось ".

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

Тепер, де цей код може зіткнутися з проблемами? Серед інших помилок, деякі "очікувані" такі:

  • download_pageможе не в змозі завантажити сторінку і кидає IOError.
  • URL може не вказувати на потрібну сторінку, або сторінка завантажена неправильно, і тому немає звернень. hitsзначить, це порожній список.
  • Веб-сторінку було змінено, можливо, зробивши наші припущення про сторінку невірними. Можливо, ми очікуємо 4 згадки про Abe Vigoda, але зараз ми знаходимо 5.
  • З певних причин hits[0]може не бути рядок форми "Abe Vigoda - це щось ", і тому її неможливо правильно розібрати.

Перший випадок насправді не є проблемою для мене: IOErrorанульовано і може оброблятись абонентом моєї функції. Тож давайте розглянемо інші випадки, і як я можу їх вирішити. Але спочатку припустимо, що ми реалізуємо parse_abe_statusнайглупішим можливим способом:

def parse_abe_status(s):
    return s[13:]

А саме, він не робить перевірки помилок. Тепер про варіанти:

Варіант 1: Повернення None

Я можу сказати абоненту, що щось пішло не так, повернувшись None:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    if not hits:
        return None

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Якщо абонент отримує Noneвід моєї функції, він повинен припустити, що про Абе Вігоду не згадувалось, і щось пішло не так. Але це досить невиразно, правда? І це не допомагає випадку, коли hits[0]це не те, що ми думали, що це було.

З іншого боку, ми можемо внести деякі винятки:

Варіант 2: Використання виключень

Якщо hitsвін порожній, під IndexErrorчас спроби буде видано заповіт hits[0]. Але не слід сподіватися на того, хто телефонує, що впорається з IndexErrorмоєю функцією, оскільки він не має уявлення, звідки це IndexErrorбуло; це могло бути кинуто find_all_mentions, бо всі він знає. Таким чином, ми створимо спеціальний клас виключень для вирішення цього питання:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

А що робити, якщо сторінка змінилася і з’явилася несподівана кількість звернень? Це не катастрофічно, оскільки код все ще може працювати, але абонент може захотіти бути особливо обережним або він може захотіти записати попередження. Тому я кину попередження:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Нарешті, ми можемо виявити, що statusне є ні живим, ні мертвим. Можливо, чомусь сьогодні це виявилося comatose. Тоді я не хочу повертатися False, оскільки це означає, що Абе мертвий. Що мені тут робити? Киньте виняток, напевно. Але який вид? Чи слід створити спеціальний клас виключень?

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    if status not in ['alive', 'dead']:
        raise SomeTypeOfError("Status is an unexpected value.")

    # he's either alive or dead
    return status == "alive"

Варіант 3: Десь посередині

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

Відповіді:


17

Рекомендація в Python - використовувати винятки, щоб вказати на збій. Це справедливо навіть у тому випадку, якщо ви очікуєте невдач на регулярній основі.

Подивіться на це з погляду абонента вашого коду:

my_status = get_abe_status(my_url)

Що робити, якщо ми повернемо None? Якщо абонент конкретно не обробляє випадок, коли get_abe_status не вдався, він просто спробує продовжувати роботу з тим, що my_stats є None. Це може призвести до важкої діагностики помилок. Навіть якщо ви перевірите наявність None, цей код не має поняття, чому get_abe_status () не вдалося.

Але що робити, якщо ми підняли виняток? Якщо абонент конкретно не обробляє цей випадок, виняток поширюється вгору, врешті-решт потрапляючи до обробника винятків за замовчуванням. Це може бути не те, що ви хочете, але краще, ніж введення тонкої помилки в іншому місці програми. Крім того, виняток дає інформацію про те, що пішло не так, що втрачено в першій версії.

З погляду абонента, просто зручніше отримати виняток, ніж значення повернення. І це стиль python, щоб використовувати винятки, щоб вказати, що умови відмови не повертають значення.

Деякі з них сприйматимуть іншу точку зору і стверджують, що ви повинні використовувати виключення лише у випадках, коли ви насправді не очікуєте, що це станеться. Вони стверджують, що нормально запущений біг не повинен створювати жодних винятків. Однією з причин цього є те, що винятки є надзвичайно неефективними, але насправді це не стосується Python.

Кілька пунктів у вашому коді:

try:
    hits[0]
except IndexError:
    raise NotFoundError("No mentions found.")

Це дійсно заплутаний спосіб перевірити наявність порожнього списку. Не наводьте винятку, щоб просто щось перевірити. Використовуйте if.

# say we expect four hits...
if len(hits) != 4:
    raise Warning("An unexpected number of hits.")
    logger.warning("An unexpected number of hits.")

Ви розумієте, що рядок logger.warning ніколи не буде працювати правильно?


1
Дякую (запізніло) за вашу відповідь. Це, разом з переглядом опублікованого коду, покращило моє відчуття того, коли і як кинути виняток.
jme

4

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

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

Я хочу навести приклад, щоб показати вам, наскільки драматична різниця в ментальності від C ++ / Java. Цикл для циклу в C ++ зазвичай виглядає приблизно так:

for(int i = 0; i != myvector.size(); ++i) ...

Спосіб подумати над цим: доступ до myvector[k]місця k> = myvector.size () спричинить виняток. Тому ви могли в принципі написати це (дуже незграбно) як спробу.

    for(int i = 0; ; ++i)  {
        try {
           ...
        } catch (& std::out_of_range)
             break

Або щось подібне. Тепер розглянемо, що відбувається в python for loop:

for i in range(1):
    ...

Як це працює? Цикл for бере результат діапазону (1) і викликає iter () на ньому, захоплюючи до нього ітератор.

b = range(1).__iter__()

Потім він викликає наступне на ньому при кожній ітерації циклу, поки ...:

>>> next(b)
0
>>> next(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Іншими словами, цикл for for python - це насправді спроба, за винятком маскування.

Що стосується конкретного питання, пам’ятайте, що винятки зупиняють нормальне виконання функції, і їх потрібно вирішувати окремо. У Python ви повинні вільно їх кидати, коли немає сенсу виконувати решту коду у вашій функції, та / або жодне з повернень не відображає правильно те, що трапилось у функції. Зауважте, що повернення достроково з функції відрізняється: повернення рано означає, що ви вже з'ясували відповідь і не потрібен решта коду, щоб з'ясувати відповідь. Я кажу, що винятки слід кидати, коли відповідь не відома, а решта коду для визначення відповіді не може бути розумно виконана. Тепер "правильно відобразити" себе, наприклад, які винятки ви вирішите кинути, - це все питання документації.

Що стосується вашого конкретного коду, я б сказав, що будь-яка ситуація, яка спричиняє хіти, є порожнім списком. Чому? Ну а спосіб налаштування вашої функції не може визначити відповідь без розбору звернень. Отже, якщо звернення не підлягають аналізу, або тому, що URL-адреса погана, або через те, що звернення порожні, функція не може відповісти на питання, а насправді навіть не може намагатися.

У цьому конкретному випадку я б заперечував, що навіть якщо вам вдасться розібратися і не отримати розумної відповіді (живої чи мертвої), то все одно слід кидати. Чому? Тому що функція повертає булеву форму. Повернення жодної не є дуже небезпечним для вашого клієнта. Якщо вони встановлять прапорець у полі "Немає", помилок не буде, вони просто мовчки трактуються як Неправдиві. Таким чином, ваш клієнт, як правило, завжди повинен робити "if None", так чи інакше перевіряйте, чи він не хоче мовчазних збоїв ... так що ви, мабуть, просто кинете.


2

Ви повинні використовувати винятки, коли відбувається щось виняткове . Тобто те, що не повинно статися при належному використанні програми. Якщо споживач вашого методу дозволений та очікуваний для пошуку того, чого не знайдеться, то "не знайдено" - це не винятковий випадок. У цьому випадку вам слід повернути null або "None" або {}, або щось, що вказує на порожній набір повернення.

Якщо, з іншого боку, ви дійсно сподіваєтесь, що споживачі вашого методу завжди (якщо вони як-небудь не викручують) знаходять те, що шукають, то не знаходження цього було б винятком, і вам слід піти з цим.

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


1
Якщо ви вирішили, що не знайти значення допустимо, будьте уважні до того, що ви використовуєте для вказівки на те, що сталося. Якщо ваш метод повинен повернути a, Stringа ви вибрали "None" як свій показник, це означає, що ви повинні бути обережними, щоб "None" ніколи не був дійсним значенням. Також зауважте, що існує різниця між переглядом даних та не знаходженням значення та неможливістю отримання даних, тому ми не можемо їх знайти. Отриманий однаковий результат для цих двох випадків означає, що ви не маєте видимості, як тільки не отримаєте жодної цінності, коли очікуєте, що буде такий.
unholysampler

Блоки вбудованого коду позначені за допомогою зворотних посилань (`), можливо, саме це ви мали намір зробити з" None "?
Ізката

3
Я боюся, що це абсолютно неправдиво в Python. Ви застосовуєте міркування стилю C ++ / Java на іншій мові. Python використовує винятки для позначення кінця циклу для циклу; це доволі незвично.
Нір Фрідман

2

Якби я писав функцію

 def abe_is_alive():

Я хотів би написати його return Trueабо Falseв тих випадках , коли я абсолютно впевнений в тій або іншій, і raiseпомилка в будь-якому іншому випадку (наприклад raise ValueError("Status neither 'dead' nor 'alive'")). Це тому, що функція, що викликає мою, очікує булевого рівня, і якщо я не можу забезпечити це з певністю, регулярний потік програми не повинен продовжуватися.

Щось на зразок вашого прикладу отримання іншої кількості "хітів", ніж очікувалося, я б, мабуть, проігнорував; доки один із хітів все ще відповідає моєму шаблону "Абе Вігода {мертвий | живий}", це добре. Це дозволяє переставляти сторінку, але все ж отримує відповідну інформацію.

Швидше ніж

try:
    hits[0] 
except IndexError:
    raise NotFoundError

Я б перевірив прямо:

if not hits:
    raise NotFoundError

оскільки це, як правило, "дешевше", а потім встановлення try.

Я згоден з вами щодо IOError; Я також не намагався б обробляти помилки під час підключення до веб-сайту - якщо ми не можемо, з якоїсь причини, це не підходяще місце для обробки (оскільки це не допомагає нам відповісти на наше запитання), і воно повинно пройти вихід у функцію виклику.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.