Являются ли исключения для лучшего контроля потока в Python?

15

Я читаю "Изучение Python" и наткнулся на следующее:

Определяемые пользователем исключения также могут сигнализировать об ошибках. Например, процедура поиска может быть закодирована, чтобы вызвать исключение при обнаружении совпадения, вместо того, чтобы возвращать флаг состояния для интерпретации вызывающей стороной. Далее обработчик исключений try / exception / else выполняет работу тестера возвращаемого значения if / else:

class Found(Exception): pass

def searcher():
    if ...success...:
        raise Found()            # Raise exceptions instead of returning flags
    else:
        return

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

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

Это ( Являются ли исключения в качестве потока управления серьезным антипаттерном? Если да, то почему? ) Также несколько комментаторов заявляют, что этот стиль - Pythonic. На чем это основано?

ТИА

JB
источник

Ответы:

25

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

  • В C ++, метание исключения является очень дорогостоящими из - за «стек разматывание». Каждое объявление локальной переменной похоже на withоператор в Python, и объект в этой переменной может запускать деструкторы. Эти деструкторы выполняются как при возникновении исключения, так и при возврате из функции. Эта «RAII-идиома» является неотъемлемой особенностью языка и очень важна для написания надежного и корректного кода - поэтому RAII по сравнению с дешевыми исключениями был компромиссом, который C ++ решил сделать для RAII.

  • В ранних версиях C ++ большая часть кода не была написана безопасным для исключений способом: если вы на самом деле не используете RAII, легко утечь память и другие ресурсы. Таким образом, создание исключений сделает этот код неправильным. Это больше не разумно, поскольку даже стандартная библиотека C ++ использует исключения: вы не можете притворяться, что исключения не существуют. Тем не менее, исключения по-прежнему являются проблемой при объединении кода C с C ++.

  • В Java каждое исключение имеет связанную трассировку стека. Трассировка стека очень полезна при отладке ошибок, но затрачивается впустую, когда исключение никогда не печатается, например, потому что оно использовалось только для потока управления.

Поэтому в этих языках исключения являются «слишком дорогими» для использования в качестве потока управления. В Python это не проблема, а исключения намного дешевле. Кроме того, язык Python уже страдает от некоторых накладных расходов, которые делают стоимость исключений незаметной по сравнению с другими конструкциями потока управления: например, проверка наличия записи dict с помощью явного теста членства if key in the_dict: ...обычно выполняется так же быстро, как простой доступ к записи the_dict[key]; ...и проверка, если вы получить ключевую ошибку. Некоторые интегральные языковые функции (например, генераторы) разработаны с точки зрения исключений.

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

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

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

Культура Python, как правило, склоняется в пользу исключений, но ее легко обойти. Представьте себе list_contains(the_list, item)функцию, которая проверяет, содержит ли список элемент, равный этому элементу. Если результат передается через исключения, это очень раздражает, потому что мы должны назвать его так:

try:
  list_contains(invited_guests, person_at_door)
except Found:
  print("Oh, hello {}!".format(person_at_door))
except NotFound:
  print("Who are you?")

Возвращение bool было бы намного понятнее:

if list_contains(invited_guests, person_at_door):
  print("Oh, hello {}!".format(person_at_door))
else:
  print("Who are you?")

Если функция уже должна возвращать значение, то возвращение специального значения для особых условий довольно подвержено ошибкам, потому что люди забудут проверить это значение (это, вероятно, является причиной 1/3 проблем в C). Исключение обычно более правильное.

Хорошим примером является pos = find_string(haystack, needle)функция, которая ищет первое вхождение needleстроки в строке haystack и возвращает начальную позицию. Но что, если в их стоге сена нет струны иглы?

Решение C и имитация Python - вернуть специальное значение. В C это нулевой указатель, в Python это -1. Это приведет к неожиданным результатам, когда позиция используется в качестве строкового индекса без проверки, особенно если -1это допустимый индекс в Python. В С ваш указатель NULL, по крайней мере, даст вам ошибку.

В PHP возвращается специальное значение другого типа: логическое значение FALSEвместо целого числа. Как оказалось, на самом деле это не лучше из-за неявных правил преобразования языка (но учтите, что и в Python логические значения могут использоваться как целые числа!). Функции, которые не возвращают согласованный тип, обычно считаются очень запутанными.

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

 try:
   pos = find_string(haystack, needle)
   do_something_with(pos)
 except NotFound:
   ...

В качестве альтернативы всегда можно использовать возвращаемый тип, который не может быть использован напрямую, но должен быть сначала развернут, например, кортеж result-bool, где логическое значение указывает, произошло ли исключение или пригоден ли результат. Потом:

pos, ok = find_string(haystack, needle)
if not ok:
  ...
do_something_with(pos)

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

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

Амон
источник
1
Спасибо за подробный ответ. Я пришел из других языков, таких как Java, где «исключения только для исключительных условий», так что для меня это ново. Существуют ли какие-либо разработчики ядра Python, которые когда-либо заявляли об этом так же, как и в других руководствах по Python, таких как EAFP, EIBTI и т. Д. (В списке рассылки, в блоге и т. Д.). Я бы хотел привести официальную цитату, если другой разработчик / босс спрашивает. Благодарность!
JB
Перегружая метод fillInStackTrace вашего пользовательского класса исключений просто для немедленного возврата, вы можете сделать его очень дешевым для повышения, так как обход стека обходится дорого, и вот где это делается.
Джон Коуэн
Ваша первая пуля в вашем вступлении поначалу сбивала с толку. Может быть , вы могли бы изменить его на что - то вроде «код обработки исключений в современном C ++ является нулевой стоимостью, но бросает исключение дорого»? (для скрытных: stackoverflow.com/questions/13835817/… ) [править: редактировать предложено]
phresnel
Обратите внимание, что для операций поиска в словаре использование collections.defaultdictили my_dict.get(key, default)делает код намного понятнееtry: my_dict[key] except: return default
Стив Барнс
11

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

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

# Using validity flag
valid, val = readbyte(source)
while valid:
    processbyte(val)
    valid, val = readbyte(source)
tidy_up()

альтернативно:

# With exceptions
try:
   val = readbyte(source)
   processbyte(val) # Note if a problem occurs here it will also raise an exception
except Exception: # Use a specific exception here!
   tidy_up()

Но с тех пор, как PEP 343 был внедрен и перенесен обратно, все это было аккуратно заключено в withзаявлении. Вышесказанное становится очень питоническим:

with open(source) as input:
    for val in input.readbyte(): # This line will raise a StopIteration exception an call input.__exit__()
        processbyte(val) # Not called if there is nothing read

В python3 это стало:

for val in open(source, 'rb').read():
     processbyte(val)

Я настоятельно призываю вас прочитать PEP 343, в котором приводится справочная информация, обоснование, примеры и т. Д.

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

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

Стив Барнс
источник
Вы говорите: «НЕТ! В общем, исключения не считаются хорошей практикой управления потоками, за исключением одного класса кода». Где обоснование этого решительного утверждения? Этот ответ, кажется, сосредоточен на итерационном случае. Есть много других случаев, когда люди могут подумать об использовании исключений. В чем проблема с этим в других случаях?
Даниэль Уолтрип
@DanielWaltrip Я вижу слишком много кода на разных языках программирования, где используются исключения, часто слишком широко, когда простое ifбудет лучше. Когда дело доходит до отладки и тестирования или даже рассуждений о поведении вашего кода, лучше не полагаться на исключения - они должны быть исключением. В производственном коде я видел вычисление, которое возвращалось с помощью метода throw / catch, а не простого возврата, это означало, что когда вычисление достигло ошибки деления на ноль, оно возвращало случайное значение, а не сбой, где произошла ошибка, что вызвало несколько часов отладки.
Стив Барнс
Привет @SteveBarnes, пример деления на ноль прилова ошибок звучит как плохое программирование (со слишком общим уловом, который не вызывает повторного исключения).
wTyeRogers
Ваш ответ, кажется, сводится к: A) "НЕТ!" B) Есть достоверные примеры того, как поток управления через исключения работает лучше, чем альтернативы. C) Исправление кода вопроса для использования генераторов (которые используют исключения в качестве потока управления, и делают это хорошо). Хотя ваш ответ, как правило, носит информативный характер, в нем, похоже, отсутствуют аргументы в поддержку пункта А, который, будучи выделенным жирным шрифтом и, по-видимому, основным утверждением, я бы ожидал некоторой поддержки. Если бы это было поддержано, я бы не понизил ваш ответ. Увы ...
wTyeRogers