Является ли хорошей практикой использование 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 части, и я также добавляю третью.
- В чем причина существования try-except-else?
- Поощряет ли шаблон try-except-else или Python в целом использование исключений для управления потоком?
- В любом случае, когда использовать исключения?
Вопрос 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: В какой ситуации вы должны разработать свою программу для выдачи исключений?
Это спорный разговор о том, является ли использование исключения в качестве потока управления анти-шаблоном . Потому что, как только для данной функции будет принято дизайнерское решение, будет определен и ее шаблон использования, и тогда у вызывающей стороны не будет другого выбора, кроме как использовать ее таким образом.
Итак, давайте вернемся к основам, чтобы увидеть, когда функция лучше будет производить свой результат, возвращая значение или создавая исключения.
В чем разница между возвращаемым значением и исключением?
У них разные «радиусы взрыва». Возвращаемое значение доступно только непосредственному вызывающему абоненту; исключение может быть автоматически ретранслировано на неограниченное расстояние, пока оно не будет обнаружено.
Характер их распространения различен. Возвращаемое значение - это по определению один фрагмент данных (даже если вы можете вернуть составной тип данных, такой как словарь или объект-контейнер, технически это все равно одно значение). Механизм исключения, напротив, позволяет возвращать несколько значений (по одному) через соответствующий выделенный канал. Здесь каждый
и блок считается собственным выделенным каналом.
Таким образом, каждый отдельный сценарий должен использовать один подходящий механизм.
Все обычные случаи лучше возвращать через возвращаемое значение, потому что вызывающим, скорее всего, потребуется немедленно использовать это возвращаемое значение. Подход с возвращаемым значением также позволяет встраивать уровни вызывающих в стиле функционального программирования. Большой радиус взрыва механизма исключения и несколько каналов здесь не помогают. Например, было бы нелогично, если бы какая-либо функция с именем
дает результат счастливого пути как исключение. (На самом деле это не надуманный пример. Есть одна практика для реализации чтобы использовать исключение, чтобы отправить значение обратно в середине глубокой рекурсии.) Если вызывающий, скорее всего, забудет обработать дозор ошибки из возвращаемого значения, вероятно, будет хорошей идеей использовать характеристику исключения # 2, чтобы спасти вызывающего абонента от его скрытой ошибки. Типичным не примером может быть
, к сожалению, его возвращаемое значение или же может вызвать ошибку в вызывающей стороне. Если индикатор ошибки столкнется с нормальным значением в пространстве имен результатов, почти наверняка будет использовано исключение, потому что вам придется использовать другой канал для передачи этой ошибки.
Если нормальный канал, то есть возвращаемое значение, уже используется в счастливом пути, И счастливый путь НЕ имеет сложного управления потоком, у вас нет другого выбора, кроме как использовать исключение для управления потоком. Люди продолжают говорить о том, как Python использует
исключение для завершения итерации, и использовать его для оправдания «использования исключения для управления потоком». Но ИМХО, это всего лишь практический выбор в конкретной ситуации, он не обобщает и не прославляет «использование исключения для управления потоком».
На этом этапе, если вы уже приняли обоснованное решение о том, будет ли ваша функция генерировать только возвращаемое значение или также вызывать исключения, или если эта функция предоставляется существующей библиотекой, так что ее поведение уже давно определено, у вас мало выбор при написании своего звонящего. Стоит ли использовать
В частности, если вы реализуете библиотеку среднего уровня
В общем, я думаю, использовать ли исключение в качестве потока управления - это спорный вопрос.
ОП, ВЫ ПРАВИЛЬНО. Остальное после 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 центов здесь. Я должен прийти и почистить большую часть кода, написанного первокурсниками из колледжских разработчиков, которые думают, что они умны и хотят писать код каким-то сверхурочным и сверхэффективным способом, когда это просто делает беспорядок попробовать и прочитать / изменить позже. Я голосую за удобочитаемость каждый день и дважды по воскресеньям.