Когда и как я должен использовать исключения?

20

Настройка

Мне часто бывает трудно определить, когда и как использовать исключения. Давайте рассмотрим простой пример: предположим, я зачищаю веб-страницу, скажем « http://www.abevigoda.com/ », чтобы определить, жива ли еще Абе Вигода. Для этого все, что нам нужно сделать, это загрузить страницу и посмотреть, когда появляется фраза «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 is нечто » и возвращает часть « что-то ».

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

Теперь, где этот код может столкнуться с проблемами? Среди других ошибок, некоторые «ожидаемые»:

  • 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. Цикл for в 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:

for i in range(1):
    ...

Как это работает? Цикл for берет результат range (1) и вызывает iter (), захватывая итератор.

b = range(1).__iter__()

Затем он вызывает следующий на каждой итерации цикла, пока ...:

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

Другими словами, цикл for в python - это на самом деле попытка, за исключением маскировки.

Что касается конкретного вопроса, помните, что исключения останавливают нормальное выполнение функции и должны рассматриваться отдельно. В Python вы должны свободно генерировать их всякий раз, когда нет смысла выполнять остальную часть кода в вашей функции, и / или ни один из возвратов не отражает правильно то, что произошло в функции. Обратите внимание, что ранний возврат из функции отличается: возврат рано означает, что вы уже выяснили ответ и вам не нужен остальной код, чтобы выяснить ответ. Я говорю, что исключения должны создаваться, когда ответ неизвестен, а остальная часть кода для определения ответа не может быть разумно выполнена. Теперь «правильно отразить» себя, например, какие исключения вы выбрасываете - все это вопрос документации.

В случае вашего конкретного кода, я бы сказал, что любая ситуация, которая приводит к тому, что попадания будут пустым списком, должна генерироваться. Почему? Что ж, как настроена ваша функция, нет способа определить ответ без разбора хитов. Таким образом, если попадания не разбираются, либо потому, что URL-адрес плохой, либо потому что совпадения пустые, то функция не может ответить на вопрос и даже не может даже попытаться.

В этом конкретном случае я бы сказал, что даже если вам удастся разобрать и не получить разумного ответа (живого или мертвого), вам все равно следует бросить. Почему? Потому что функция возвращает логическое значение. Возвращение Ни один очень опасно для вашего клиента. Если они выполнят проверку «Нет», сбоя не будет, он будет просто рассматриваться как Ложный. Итак, ваш клиент в основном всегда должен делать проверку if is None, если он не хочет молчаливых сбоев ... так что вы, вероятно, должны просто бросить.

Нир Фридман
источник
2

Вы должны использовать исключения, когда происходит что-то исключительное . То есть то, что не должно происходить при правильном использовании приложения. Если для потребителя вашего метода допустимо и ожидается, что пользователь найдет что-то, что не будет найдено, то «not found» не является исключительным случаем. В этом случае вы должны вернуть null или «None» или {}, или что-то, указывающее пустой набор возврата.

Если, с другой стороны, вы действительно ожидаете, что потребители вашего метода всегда (если они не облажались) найдут то, что искали, тогда не обнаружение этого будет исключением, и вы должны пойти на это.

Ключевым моментом является то, что обработка исключений может быть дорогой - исключения должны собирать информацию о состоянии вашего приложения, когда они происходят, например трассировку стека, чтобы помочь людям понять, почему они произошли. Я не думаю, что это то, что вы пытаетесь сделать.

Мэтью Флинн
источник
1
Если вы решите, что недопустимо нахождение значения, будьте осторожны с тем, что вы используете, чтобы указать, что произошло. Если ваш метод должен возвращать a Stringи вы выбираете «None» в качестве индикатора, это означает, что вы должны быть осторожны, чтобы «None» никогда не было действительным значением. Также обратите внимание на то, что существует разница между просмотром данных и невозможностью найти значение и невозможностью извлечь данные, поэтому мы не можем найти данные. Наличие одинакового результата для этих двух случаев означает, что у вас нет никакой видимости, когда вы не получаете никакой ценности, когда ожидаете, что она будет.
unholysampler
Блоки встроенного кода помечены обратными чертами (`), возможно, это то, что вы хотели сделать с" None "?
Изката
3
Боюсь, это абсолютно неверно в Python. Вы применяете рассуждения в стиле C ++ / Java к другому языку. Python использует исключения, чтобы указать конец цикла for; это довольно необычно.
Нир Фридман
2

Если бы я писал функцию

 def abe_is_alive():

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

Что-то вроде вашего примера получения количества «хитов», отличного от ожидаемого, я бы, вероятно, проигнорировал; до тех пор, пока один из хитов по-прежнему соответствует моему шаблону "Abe Vigoda is {dead | alive}", это нормально. Это позволяет перестроить страницу, но все равно получает соответствующую информацию.

Скорее, чем

try:
    hits[0] 
except IndexError:
    raise NotFoundError

Я бы проверил явно:

if not hits:
    raise NotFoundError

так как это, как правило, «дешевле», чем настройка try.

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

jonrsharpe
источник