Чи є хорошою практикою використовувати try-крім-ще в Python?


439

Час від часу в 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

Відповіді:


666

"Я не знаю, чи це невідомо, але мені не подобається таке програмування, оскільки воно використовує винятки для контролю потоку".

У світі Python використання винятків для контролю потоку є звичайним і нормальним.

Навіть розробники ядра Python використовують винятки для управління потоком, і цей стиль сильно введений у мову (тобто протокол ітератора використовує StopIteration для припинення циклу сигналу).

Крім того, стиль "спробу-окрім" використовується для запобігання умовам перегонів, притаманним деяким конструкціям "погляд перед тобою" . Наприклад, тестування os.path.exists призводить до застарілої інформації до моменту її використання. Так само Queue.full повертає інформацію, яка може бути несвіжою . Стиль " try-osim-else" створить більш надійний код у цих випадках.

"Наскільки я розумію, що винятки не є помилками, їх слід використовувати лише у виняткових умовах"

В деяких інших мовах це правило відображає їхні культурні норми, відображені в їхніх бібліотеках. "Правило" також базується частково на міркуваннях для цих мов.

Культурна норма Пітона дещо інша. У багатьох випадках ви повинні використовувати винятки для контрольного потоку. Крім того, використання винятків у Python не уповільнює оточуючий код та код виклику, як це відбувається у деяких зібраних мовах (тобто CPython вже реалізує код для перевірки винятків на кожному кроці, незалежно від того, ви фактично використовуєте винятки чи ні).

Іншими словами, ваше розуміння того, що "винятки є винятковими" - це правило, яке має сенс для деяких інших мов, але не для Python.

"Однак, якщо він включений до самої мови, це має бути вагомою причиною, чи не так?"

Крім того, що допомагають уникнути умов перегонів, винятки також дуже корисні для витягування помилок із зовнішніх циклів. Це необхідна оптимізація в інтерпретованих мовах, які не мають автоматичного руху коду інваріантного коду .

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

Тут є приємна публікація в блозі про необхідність винятків .

Також дивіться цю відповідь на переповнення стека: чи справді винятки є винятковими помилками?

"У чому причина спроби, крім того, що існує ще?"

Інший пункт сам по собі цікавий. Він працює, коли не є винятком, але перед остаточним пунктом. Це його головне призначення.

Без іншого пункту, єдиним варіантом запуску додаткового коду до фіналізації буде незграбна практика додавання коду до спробу-клаузу. Це незграбно, оскільки це ризикує збільшити винятки в коді, який не мав бути захищений пробним блоком.

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

Іншим випадком використання пункту 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='')

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


28
"Це незграбно, оскільки це ризикує збільшити винятки в коді, який не мав бути захищений пробним блоком." Це найважливіше навчання тут
Фелікс Домбек

2
Дякую за відповідь. Для читачів, які шукають приклади спроб, крім іншого, подивіться на метод копіювання
shutil

2
Справа в тому, що інше застереження виконується лише тоді, коли клавіша спробу буде успішною.
Джонатан

172

У чому причина спроби, окрім іншого, існувати?

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

elseПункт буде виконуватися , якщо не було ніяких помилок, і не виконує цей код в tryблоці, ви уникнете зловити несподівану помилку. Знову ж таки, виявлення несподіваної помилки може приховати помилки.

Приклад

Наприклад:

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

У наборі "спробувати, за винятком" є два необов'язкові пропозиції 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 буде виконуватися незважаючи ні на що, навіть якщо інший рядок оцінюється випискою return.

Розбитий псевдокодом

Це може допомогти розбити це в найменшій формі, що демонструє всі функції, з коментарями. Припустимо, цей синтаксично правильний (але не піддається виконанню, якщо не визначені імена) псевдо-код функціонує.

Наприклад:

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 ви можете створити новий виняток і зберегти backtrace з ланцюжком виключень:

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

Я деталізую тут свою відповідь .


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

Прочитайте Дзен Пітона , розуміючи, що існують принципи, які перебувають у напрузі, і будьте обережні до догми, яка занадто сильно покладається на будь-яке з висловлювань у ній.


12

Дивіться наступний приклад, який ілюструє все про спробу, крім іншого, нарешті:

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
Це чудовий, простий приклад, який швидко демонструє повну спробу застереження, не вимагаючи, щоб хтось (хто може дуже поспішати) прочитати тривале абстрактне пояснення. (Звичайно, коли вони вже не поспішають, вони повинні повернутися і прочитати повний конспект.)
GlobalSoftwareSociety

6

Ви повинні бути обережними щодо використання остаточного блоку, оскільки це не те саме, що використання блоку else у спробі, за винятком. Остаточний блок буде запущений незалежно від результату спроби, крім.

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"
   ....:

Я знаю, що нарешті завжди виконується, і саме тому його можна використовувати на нашу користь, завжди встановлюючи значення за замовчуванням, тому у випадку винятку воно повертається, якщо ми не хочемо повертати таке значення у випадку винятку, це достатньо для видалення остаточного блоку. Btw, використання пропуску на виняток - це те, що я ніколи не зробив би :)
Хуан Антоніо Гомес Моріано

@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 та авторитетом у всіх пітонічних речах!)
Rajiv Bakulesh Shah

4

Просто я б сказав, що ніхто більше не опублікував цю думку

уникайте elseзастережень, try/excepts оскільки вони незнайомі більшості людей

На відміну від ключових слів try, exceptі finally, значення elseпункту не є само собою зрозумілим; він менш читабельний. Оскільки він використовується не дуже часто, люди, які читають ваш код, захочуть ще раз перевірити документи, щоб переконатися, що вони розуміють, що відбувається.

(Я пишу цю відповідь саме тому, що я знайшов try/except/elseу своїй кодовій базі, і це спричинило момент wtf і змусило мене зробити якийсь гуглінг).

Отже, де б я не бачив код, як приклад ОП:

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блоці, виділяється одним рівнем.


1
Контр-аргумент диявольського аргументу: чим більше людей ним користується, тим сприйнятішим стане. Просто їжа для роздумів, хоча я згоден, що читабельність - це імпорт. Однак, колись хтось зрозуміє спробу, я б заперечував, що це набагато легше читати, ніж альтернатива в багатьох випадках.
боб

2

Це мій простий фрагмент про те, як зрозуміти блок try, крім іншого, нарешті, в 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))

Давайте спробуємо розділити 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

1
Я думаю, що це не може пояснити, чому ви не можете просто поставити інший код у спробу
Моїмі

-4

ОП, ВИ ПРАВИЛЬНІ. Інше після спроби / крім у 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()"

Це набагато зрозуміліше, ніж інше застереження. Інше після спроби / за винятком часто не пишеться, тому потрібен момент, щоб зрозуміти, які наслідки є.

Тільки тому, що ВИ МОЖЕТЕ зробити щось, не означає, що ТРІБУВАТИ щось.

До мов додано багато функцій, оскільки хтось подумав, що це може стати в нагоді. Проблема полягає в тому, що чим більше можливостей, тим менш чіткими і очевидними є речі, оскільки люди зазвичай не використовують ці дзвіночки.

Просто мої 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.
alastair

якщо ... за винятком ... ще є легше читати, інакше вам доведеться прочитати "о, після блоку спробу, і не виняток, перейти до оператора поза блоком" краще imo. Крім того, корисно залишати незапущені заяви поза початковим блоком спробу.
коуберт

1
@DougR. "Ви оглядаєтеx = blah() щоб знайти джерело винятку", маючи tracebackчому ви перевіряєте джерело виключення з неправильного місця?
nehem
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.