Является ли хорошей практикой использование try-кроме-еще в Python?

Время от времени в Python я вижу блок:

try:
   try_this(whatever)
except SomeException as exception:
   #Handle exception
else:
   return something

В чем причина существования "попробовать, кроме как"?

Мне не нравится такой вид программирования, поскольку он использует исключения для управления потоком данных. Однако, если он включен в язык, для этого должна быть веская причина, не так ли?

Насколько я понимаю, исключения не являются ошибками, и их следует использовать только для исключительных условий (например, я пытаюсь записать файл на диск, и больше нет места, или, возможно, у меня нет разрешения), а не для потока контроль.

Обычно я обрабатываю исключения как:

something = some_default_value
try:
    something = try_this(whatever)
except SomeException as exception:
    #Handle exception
finally:
    return something

Или, если я действительно не хочу ничего возвращать, если произойдет исключение, то:

try:
    something = try_this(whatever)
    return something
except SomeException as exception:
    #Handle exception

11 ответов

Решение

"Я не знаю, происходит ли это по незнанию, но мне не нравится такой вид программирования, поскольку он использует исключения для управления потоком".

В мире Python использование исключений для управления потоком является обычным явлением.

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

Кроме того, стиль "попробуй за исключением" используется для предотвращения условий гонки, присущих некоторым конструкциям "смотреть перед прыжком". Например, тестирование os.path.exists приводит к получению информации, которая может устареть к тому времени, когда вы ее используете. Аналогично, Queue.full возвращает информацию, которая может быть устаревшей. В этом случае стиль try-кроме-else даст более надежный код.

"Если я понимаю, что исключения не являются ошибками, они должны использоваться только для исключительных условий"

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

Культурная норма Python несколько иная. Во многих случаях вы должны использовать исключения для потока управления. Кроме того, использование исключений в Python не замедляет окружающий код и вызывающий код, как это происходит в некоторых скомпилированных языках (то есть CPython уже реализует код для проверки исключений на каждом шаге, независимо от того, используете ли вы на самом деле исключения или нет).

Другими словами, ваше понимание того, что "исключения являются исключительными", является правилом, которое имеет смысл в некоторых других языках, но не для Python.

"Однако, если он включен в сам язык, для этого должна быть веская причина, не так ли?"

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

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

Здесь есть хорошая запись в блоге об обязательности исключений.

Кроме того, см. Ответ "Переполнение стека": действительно ли исключения являются исключительными?

"Какова причина существования попытки, за исключением чего-либо еще?"

Само условие else интересно. Он запускается, когда нет исключения, но до предложения finally. Это его основная цель.

Без предложения else единственным вариантом запуска дополнительного кода перед финализацией была бы неуклюжая практика добавления кода в предложение try. Это неуклюже, потому что это может привести к возникновению исключений в коде, который не предназначен для защиты блоком try.

Вариант использования дополнительного незащищенного кода перед финализацией возникает не очень часто. Так что не ожидайте увидеть много примеров в опубликованном коде. Это несколько редко.

Другим вариантом использования для условия else является выполнение действий, которые должны происходить, когда исключение не происходит, и не происходят при обработке исключений. Например:

   recip = float('Inf')
   try:
       recip = 1 / f(x)
   except ZeroDivisionError:
       logging.info('Infinite result')
   else:
       logging.info('Finite result')

И, наконец, наиболее распространенное использование выражения else в блоке try предназначено для небольшого улучшения (выравнивание исключительных результатов и неисключительных результатов при одинаковом уровне отступа). Это использование всегда необязательно и не является строго необходимым.

В чем причина существования "попробовать, кроме как"?

try Блок позволяет обрабатывать ожидаемую ошибку. except блок должен перехватывать только те исключения, которые вы готовы обработать. Если вы обрабатываете неожиданную ошибку, ваш код может сделать что-то не так и скрыть ошибки.

else предложение будет выполнено, если не было ошибок, и не выполняя этот код в try блок, вы избегаете ловить неожиданную ошибку. Опять же, обнаружение неожиданной ошибки может скрыть ошибки.

пример

Например:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
else:
    return something

Пакет "try, кроме" содержит два необязательных предложения: else а также finally, Так что на самом деле try-except-else-finally,

else будет оценивать, только если нет исключения из try блок. Это позволяет нам упростить более сложный код ниже:

no_error = None
try:
    try_this(whatever)
    no_error = True
except SomeException as the_exception:
    handle(the_exception)
if no_error:
    return something

так что если мы сравним else к альтернативе (которая может создавать ошибки) мы видим, что это сокращает количество строк кода и мы можем иметь более читабельную, поддерживаемую и менее ошибочную кодовую базу.

finally

finally будет выполняться независимо от того, что, даже если другая строка оценивается с оператором возврата.

Разбит с помощью псевдокода

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

Например:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle_SomeException(the_exception)
    # Handle a instance of SomeException or a subclass of it.
except Exception as the_exception:
    generic_handle(the_exception)
    # Handle any other exception that inherits from Exception
    # - doesn't include GeneratorExit, KeyboardInterrupt, SystemExit
    # Avoid bare `except:`
else: # there was no exception whatsoever
    return something()
    # if no exception, the "something()" gets evaluated,
    # but the return will not be executed due to the return in the
    # finally block below.
finally:
    # this block will execute no matter what, even if no exception,
    # after "something" is eval'd but before that value is returned
    # but even if there is an exception.
    # a return here will hijack the return functionality. e.g.:
    return True # hijacks the return in the else clause above

Это правда, что мы могли бы включить код в else блок в try вместо этого, где бы он работал, если бы не было исключений, но что, если сам этот код вызывает исключение того типа, который мы ловим? Оставив его в try Блок скрыл бы эту ошибку.

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

Насколько я понимаю, исключения не являются ошибками

В Python большинство исключений - ошибки.

Мы можем просмотреть иерархию исключений с помощью pydoc. Например, в Python 2:

$ python -m pydoc exceptions

или Python 3:

$ python -m pydoc builtins

Даст нам иерархию. Мы можем видеть, что большинство видов Exception являются ошибками, хотя Python использует некоторые из них для таких вещей, как окончание for петли (StopIteration). Это иерархия Python 3:

BaseException
    Exception
        ArithmeticError
            FloatingPointError
            OverflowError
            ZeroDivisionError
        AssertionError
        AttributeError
        BufferError
        EOFError
        ImportError
            ModuleNotFoundError
        LookupError
            IndexError
            KeyError
        MemoryError
        NameError
            UnboundLocalError
        OSError
            BlockingIOError
            ChildProcessError
            ConnectionError
                BrokenPipeError
                ConnectionAbortedError
                ConnectionRefusedError
                ConnectionResetError
            FileExistsError
            FileNotFoundError
            InterruptedError
            IsADirectoryError
            NotADirectoryError
            PermissionError
            ProcessLookupError
            TimeoutError
        ReferenceError
        RuntimeError
            NotImplementedError
            RecursionError
        StopAsyncIteration
        StopIteration
        SyntaxError
            IndentationError
                TabError
        SystemError
        TypeError
        ValueError
            UnicodeError
                UnicodeDecodeError
                UnicodeEncodeError
                UnicodeTranslateError
        Warning
            BytesWarning
            DeprecationWarning
            FutureWarning
            ImportWarning
            PendingDeprecationWarning
            ResourceWarning
            RuntimeWarning
            SyntaxWarning
            UnicodeWarning
            UserWarning
    GeneratorExit
    KeyboardInterrupt
    SystemExit

Комментатор спросил:

Допустим, у вас есть метод, который отправляет эхо-запрос на внешний API, и вы хотите обработать исключение в классе вне оболочки API. Вы просто возвращаете e из метода в предложении исключением, где e является объектом исключения?

Нет, вы не вернете исключение, просто восстановите его с голым raise сохранить трассировку стека.

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
    raise

Или в Python 3 вы можете вызвать новое исключение и сохранить обратный след с цепочкой исключений:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
    raise DifferentException from the_exception

Я уточню в своем ответе здесь.

Python не поддерживает идею, что исключения должны использоваться только в исключительных случаях, на самом деле идиома "просить прощения, а не разрешения". Это означает, что использование исключений в качестве рутинной части управления потоком данных вполне приемлемо и, по сути, приветствуется.

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

Представьте, что у вас есть ситуация, когда вы берете некоторый пользовательский ввод, который должен быть обработан, но по умолчанию уже обрабатывается. try: ... except: ... else: ... структура делает для очень удобочитаемого кода:

try:
   raw_value = int(input())
except ValueError:
   value = some_processed_value
else: # no error occured
   value = process_value(raw_value)

Сравните с тем, как это может работать на других языках:

raw_value = input()
if valid_number(raw_value):
    value = process_value(int(raw_value))
else:
    value = some_processed_value

Обратите внимание на преимущества. Нет необходимости проверять правильность значения и анализировать его отдельно, они выполняются один раз. Код также следует более логичной последовательности: сначала идет путь к основному коду, а затем "если это не работает, сделайте это".

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

Посмотрите следующий пример, который иллюстрирует все о try-кроме-else-finally:

for i in range(3):
    try:
        y = 1 / i
    except ZeroDivisionError:
        print(f"\ti = {i}")
        print("\tError report: ZeroDivisionError")
    else:
        print(f"\ti = {i}")
        print(f"\tNo error report and y equals {y}")
    finally:
        print("Try block is run.")

Реализуйте это и приходите:

    i = 0
    Error report: ZeroDivisionError
Try block is run.
    i = 1
    No error report and y equals 1.0
Try block is run.
    i = 2
    No error report and y equals 0.5
Try block is run.

Является ли хорошей практикой использование try-кроме-иначе в Python?

Ответ заключается в том, что это зависит от контекста. Если вы делаете это:

d = dict()
try:
    item = d['item']
except KeyError:
    item = 'default'

Это показывает, что вы не очень хорошо знаете Python. Эта функциональность заключена в dict.get метод:

item = d.get('item', 'default')

try/except Блок - это гораздо более визуально загроможденный и многословный способ написания того, что может быть эффективно выполнено в одной строке атомарным методом. Есть и другие случаи, когда это правда.

Однако это не означает, что мы должны избегать обработки всех исключений. В некоторых случаях предпочтительно избегать условий гонки. Не проверяйте, существует ли файл, просто попытайтесь открыть его и перехватите соответствующую ошибку IOError. Ради простоты и читабельности, попробуйте инкапсулировать это или вычленить как подходящее.

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

Вы должны быть осторожны с использованием блока finally, поскольку это не то же самое, что использование блока else в try, кроме. Блок finally будет выполняться независимо от результата попытки, кроме.

In [10]: dict_ = {"a": 1}

In [11]: try:
   ....:     dict_["b"]
   ....: except KeyError:
   ....:     pass
   ....: finally:
   ....:     print "something"
   ....:     
something

Как уже отмечалось, использование блока else делает ваш код более читабельным и запускается только тогда, когда исключение не выдается.

In [14]: try:
             dict_["b"]
         except KeyError:
             pass
         else:
             print "something"
   ....:

Я бы сказал, что только потому, что никто другой не опубликовал это мнение

избегать else статьи в try/excepts потому что они незнакомы большинству людей

В отличие от ключевых слов try, except, а также finally, значение elseпункт не самоочевиден; это менее читабельно. Поскольку он используется не очень часто, люди, читающие ваш код, захотят перепроверить документацию, чтобы убедиться, что они понимают, что происходит.

(Я пишу этот ответ именно потому, что нашел try/except/else в моей кодовой базе, и это вызвало момент wtf и заставило меня заняться поиском в Google).

Итак, везде, где я вижу код, подобный примеру OP:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
else:
    # do some more processing in non-exception case
    return something

Я бы предпочел рефакторинг, чтобы

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
    return  # <1>
# do some more processing in non-exception case  <2>
return something
  • <1> явный возврат, ясно показывает, что в случае исключения мы закончили работу

  • <2> в качестве приятного второстепенного побочного эффекта, код, который раньше был в else блок отступает на один уровень.

Всякий раз, когда вы видите это:

try:
    y = 1 / x
except ZeroDivisionError:
    pass
else:
    return y

Или даже это:

try:
    return 1 / x
except ZeroDivisionError:
    return None

Рассмотрим это вместо этого:

import contextlib
with contextlib.suppress(ZeroDivisionError):
    return 1 / x

Это мой простой фрагмент о том, как понять блок try-исключением-else-finally в Python:

def div(a, b):
    try:
        a/b
    except ZeroDivisionError:
        print("Zero Division Error detected")
    else:
        print("No Zero Division Error")
    finally:
        print("Finally the division of %d/%d is done" % (a, b))

Давайте попробуем div 1/1:

div(1, 1)
No Zero Division Error
Finally the division of 1/1 is done

Давайте попробуем div 1/0

div(1, 0)
Zero Division Error detected
Finally the division of 1/0 is done

Я пытаюсь ответить на этот вопрос под несколько другим углом.

В вопросе ОП было 2 части, и я также добавляю третью.

  1. В чем причина существования try-except-else?
  2. Поощряет ли шаблон try-except-else или Python в целом использование исключений для управления потоком?
  3. В любом случае, когда использовать исключения?

Вопрос 1: Какова причина существования try-except-else?

На него можно ответить с тактической точки зрения. Конечно, есть причина для существовать. Единственное новое дополнение - это предложение, полезность которого сводится к его уникальности:

  • Он запускает дополнительный блок кода ТОЛЬКО КОГДА в блоке не произошло исключения.

  • Он запускает этот дополнительный блок кода ВНЕ блока (это означает, что любые потенциальные исключения, происходящие внутри блока, НЕ будут обнаружены).

  • Он запускает этот дополнительный блок кода ПЕРЕД доработка.

              db = open(...)
      try:
          db.insert(something)
      except Exception:
          db.rollback()
          logging.exception('Failing: %s, db is ROLLED BACK', something)
      else:
          db.commit()
          logging.info(
              'Successful: %d',  # <-- For the sake of demonstration,
                                 # there is a typo %d here to trigger an exception.
                                 # If you move this section into the try... block,
                                 # the flow would unnecessarily go to the rollback path.
              something)
      finally:
          db.close()
    

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

Вопрос 2: поощряет ли Python использование исключений для управления потоком?

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

EAFP

Проще просить прощения, чем разрешения. Этот общий стиль кодирования Python предполагает наличие действительных ключей или атрибутов и перехватывает исключения, если предположение оказывается ложным. Этот чистый и быстрый стиль характеризуется наличием множества утверждений «попробуй и исключь». Эта техника контрастирует со стилем LBYL, общим для многих других языков, таких как C.

Такой параграф просто описывает это, а не делает следующее:

      def make_some_noise(speaker):
    if hasattr(speaker, "quack"):
        speaker.quack()

мы бы предпочли это:

      def make_some_noise(speaker):
    try:
        speaker.quack()
    except AttributeError:
        logger.warning("This speaker is not a duck")

make_some_noise(DonaldDuck())  # This would work
make_some_noise(DonaldTrump())  # This would trigger exception

или, возможно, даже пропуская попытку ... за исключением:

      def make_some_noise(duck):
    duck.quack()

Итак, EAFP поощряет утиную печать. Но это не поощряет использование исключений для управления потоком.

Вопрос 3: В какой ситуации вы должны разработать свою программу для выдачи исключений?

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

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

В чем разница между возвращаемым значением и исключением?

  1. У них разные «радиусы взрыва». Возвращаемое значение доступно только непосредственному вызывающему абоненту; исключение может быть автоматически ретранслировано на неограниченное расстояние, пока оно не будет обнаружено.

  2. Характер их распространения различен. Возвращаемое значение - это по определению один фрагмент данных (даже если вы можете вернуть составной тип данных, такой как словарь или объект-контейнер, технически это все равно одно значение). Механизм исключения, напротив, позволяет возвращать несколько значений (по одному) через соответствующий выделенный канал. Здесь каждый и блок считается собственным выделенным каналом.

Таким образом, каждый отдельный сценарий должен использовать один подходящий механизм.

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

  • Если вызывающий, скорее всего, забудет обработать дозор ошибки из возвращаемого значения, вероятно, будет хорошей идеей использовать характеристику исключения # 2, чтобы спасти вызывающего абонента от его скрытой ошибки. Типичным не примером может быть , к сожалению, его возвращаемое значение или же может вызвать ошибку в вызывающей стороне.

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

  • Если нормальный канал, то есть возвращаемое значение, уже используется в счастливом пути, И счастливый путь НЕ имеет сложного управления потоком, у вас нет другого выбора, кроме как использовать исключение для управления потоком. Люди продолжают говорить о том, как Python использует исключение для завершения итерации, и использовать его для оправдания «использования исключения для управления потоком». Но ИМХО, это всего лишь практический выбор в конкретной ситуации, он не обобщает и не прославляет «использование исключения для управления потоком».

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

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

В общем, я думаю, использовать ли исключение в качестве потока управления - это спорный вопрос.

ОП, ВЫ ПРАВИЛЬНО. Остальное после try/ кроме в Python ужасно. это приводит к другому объекту управления потоком, где ни один не нужен:

try:
    x = blah()
except:
    print "failed at blah()"
else:
    print "just succeeded with blah"

Совершенно понятный эквивалент:

try:
    x = blah()
    print "just succeeded with blah"
except:
    print "failed at blah()"

Это гораздо понятнее, чем предложение else. Иное после try/ исключением пишется не часто, поэтому требуется время, чтобы понять, что это означает.

То, что вы МОЖЕТЕ делать что-то, не означает, что вы ДОЛЖНЫ делать что-то.

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

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

Другие вопросы по тегам