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

440

Время от времени в 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
Хуан Антонио Гомес Мориано
источник

Ответы:

668

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Другой пример встречается у бегунов на юнит-тестах:

try:
    tests_run += 1
    run_testcase(case)
except Exception:
    tests_failed += 1
    logging.exception('Failing test case: %r', case)
    print('F', end='')
else:
    logging.info('Successful test case: %r', case)
    print('.', end='')

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

Раймонд Хеттингер
источник
29
«Это неуклюже, потому что это может привести к возникновению исключений в коде, который не предназначен для защиты блоком try». Это самое важное обучение здесь
Феликс Домбек
2
Спасибо за ответ. Для читателей, ищущих примеры использования try-кроме-else, взгляните на метод copyfile в shutil github.com/python/cpython/blob/master/Lib/shutil.py#L244
suripoori
2
Дело в том, что предложение else выполняется только тогда, когда предложение try успешно выполняется.
Джонатан
173

В чем причина того, что существует только попытка?

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

поэтому, если мы сравним a 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

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

Аарон Холл
источник
upvoted! что ты обычно делаешь внутри ручки? Допустим, у вас есть метод, который отправляет эхо-запрос на внешний API, и вы хотите обработать исключение в классе за пределами оболочки API. Вы просто возвращаете e из метода в предложении исключением, где e является объектом исключения?
PirateApp
1
@PirateApp спасибо! нет, не возвращайте его, вы, вероятно, должны сделать ререйз с использованием raiseцепочки исключений или сделать цепочку исключений - но об этом подробнее и рассказано здесь: stackoverflow.com/q/2052390/541136 - я, вероятно, удалю эти комментарии после того, как видел, что ты видел их.
Аарон Холл
Большое спасибо за детали! просматриваю пост сейчас
PirateApp
36

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

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

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

Гарет Латти
источник
15

Является ли хорошей практикой использование try-исключением-else в 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 , понимая, что существуют принципы, которые находятся в напряжении, и будьте осторожны с догмой, которая слишком сильно зависит от любого из утверждений в нем.

Аарон Холл
источник
12

Посмотрите следующий пример, который иллюстрирует все о 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.
Исчисление
источник
3
Это отличный, простой пример, который быстро демонстрирует полное предложение try , не требуя, чтобы кто-то (кто может очень спешить) прочитал длинное абстрактное объяснение. (Конечно, когда они больше не спешат, им следует вернуться и прочитать полную аннотацию.)
GlobalSoftwareSociety
6

Вы должны быть осторожны с использованием блока 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"
   ....:
Greg
источник
Я знаю, что, наконец, всегда выполняется, и поэтому его можно использовать в наших интересах, всегда устанавливая значение по умолчанию, поэтому в случае исключения оно возвращается, если мы не хотим возвращать такое значение в случае исключения, оно достаточно, чтобы удалить последний блок. Кстати, использование пропуска на ловушку исключения - это то, чего я никогда бы не сделал :)
Хуан Антонио Гомес Мориано,
@Juan Антонио Гомес Мориано, мой блок кодирования только для примера. Я, вероятно, никогда не использовал бы проход
Грег
4

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

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
Раджив Бакулеш Шах
источник
1
Это не отвечает на мой вопрос, так как это был просто пример, мой друг.
Хуан Антонио Гомес
В Python исключения не являются ошибками. Они даже не исключительные. В Python нормально и естественно использовать исключения для управления потоком. Об этом свидетельствует включение contextlib.suppress () в стандартную библиотеку. Смотрите ответ Рэймонда Хеттингера здесь: stackoverflow.com/a/16138864/1197429 (Рэймонд является основным автором Python и авторитетом во всех вещах Pythonic!)
Раджив Бакулеш Шах
4

Просто потому, что никто не опубликовал это мнение, я бы сказал,

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

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

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

Поэтому везде, где я вижу код, подобный примеру 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блоке, отступает на один уровень.

hwjp
источник
1
Аргумент против дьявола: чем больше людей его используют, тем более хорошо он будет принят. Просто пища для размышлений, хотя я согласен с тем, что читабельность важна. Тем не менее, как только кто-то поймет, что такое try-else, я бы сказал, что во многих случаях он гораздо более читабелен, чем альтернатива.
Боб
2

Это мой простой фрагмент о том, как понять блок 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
zakiakhmad
источник
1
Я думаю, что это не может служить примером того, почему вы не можете просто поместить код else внутри попытки
Моджими
-4

ОП, ВЫ ПРАВИЛЬНО. Остальное после 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 центов здесь. Я должен прийти и почистить большую часть кода, написанного первокурсниками из колледжских разработчиков, которые думают, что они умны и хотят писать код каким-то сверхурочным и сверхэффективным способом, когда это просто делает беспорядок попробовать и прочитать / изменить позже. Я голосую за удобочитаемость каждый день и дважды по воскресеньям.

Кевин Дж. Райс
источник
15
Вы правы. Это совершенно ясно и эквивалентно ... если только это не ваше printутверждение. Что произойдет, если x = blah()вернет a str, но ваш оператор печати print 'just succeeded with blah. x == %d' % x? Теперь у вас есть TypeErrorсущество, созданное там, где вы не готовы с ним справиться; вы проверяете, x = blah()чтобы найти источник исключения, а его там даже нет. Я делал это (или эквивалент) не раз, когда elseбы я не допустил эту ошибку. Теперь я знаю лучше. :-D
Даг Р.
2
... и да, ты прав. Предложение elseне является красивым утверждением, и пока вы не привыкли к нему, оно не интуитивно понятно. Но тогда, ни то, ни другое, finallyкогда я впервые начал использовать его ...
Даг Р.
2
Повторять Дугу Р. это не эквивалентно, потому что исключения во время операторов в elseпредложении не перехватываются except.
Аластер
if ... кроме ... else более читабельно, в противном случае вы должны прочитать «о, после блока try, и без исключения, перейти к оператору вне блока try», поэтому использование else: имеет тенденцию к семантическому соединению синтаксиса немного лучше имо Кроме того, это хорошая практика, чтобы оставить ваши нетронутые операторы за пределами начального блока try.
Cowbert
1
@DougR. «Вы проверяете, x = blah()чтобы найти источник исключения», имея tracebackпочему вы должны проверять источник исключения из неправильного места?
Нехем