Чи є винятки для контролю потоку найкращою практикою в Python?


15

Я читаю "Навчання Python" і натрапив на таке:

Винятки, визначені користувачем, можуть також сигналізувати про умови ненормативної помилки. Наприклад, підпрограм пошуку можна кодувати, щоб створити виняток, коли знайдено збіг, а не повернути прапор статусу для абонента для інтерпретації. У наступному, обробник випробувань / за винятком / 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?

Це ( Чи є винятки, коли контрольний потік вважається серйозним антипатерном? Якщо так, то чому? ), Також декілька коментаторів стверджують, що цей стиль є пітонічним. На чому ґрунтується це?

ТІА

Відповіді:


26

Загальний консенсус "не використовуйте винятків!" здебільшого походить з інших мов і навіть там іноді застаріла.

  • У програмі C ++ викидання виключень дуже дороге через "розмотування стека". Кожна заява локальної змінної є як withвираз у Python, і об'єкт у цій змінній може запускати деструктори. Ці деструктори виконуються, коли викидається виняток, але також при поверненні з функції. Ця "ідіома RAII" є невід'ємною мовною особливістю і дуже важливо писати надійний, правильний код - тому RAII проти дешевих винятків був компромісом, який C ++ вирішив у напрямку RAII.

  • На початку C ++ багато коду не було написано безпечним для винятків способом: якщо ви фактично не використовуєте RAII, легко просочити пам'ять та інші ресурси. Отже, якщо викидати винятки, цей код буде неправильним. Це вже не розумно, оскільки навіть стандартна бібліотека C ++ використовує винятки: ви не можете робити вигляд, що винятки не існують. Однак винятки все ще є проблемою при поєднанні коду C із C ++.

  • У Java кожен виняток має пов'язаний слід стека. Трасування стека є дуже цінним при налагодженні помилок, але витрачається на витрачені зусилля, коли виняток ніколи не друкується, наприклад, тому що він використовувався лише для управління потоком.

Тож у цих мовах винятки є "занадто дорогими", щоб використовуватись як контрольний потік. У Python це менше питання, а винятки - значно дешевші. Крім того, мова Python вже страждає від деяких накладних витрат, що робить вартість винятків непомітною порівняно з іншими конструкціями контрольного потоку: наприклад, перевірка наявності запису dict із явним тестом на членство, if key in the_dict: ...як правило, настільки ж швидка, як просто доступ до запису the_dict[key]; ...та перевірка, якщо ви отримати KeyError. Деякі цілісні мовні функції (наприклад, генератори) розроблені з точки зору винятків.

Тож поки немає жодних технічних причин, щоб спеціально уникати винятків у 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?")

Повернення булінгу буде набагато зрозумілішим:

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

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

Хорошим прикладом є pos = find_string(haystack, needle)функція, яка шукає перше виникнення needleрядка в рядку `копиця сіна і повертає початкове положення. Але що робити, якщо в копиці сіна не міститься голка-рядок?

Рішення C і імітоване Python - повернути особливе значення. У C це нульовий покажчик, у Python це -1. Це призведе до дивовижних результатів, коли позиція використовується в якості рядкового індексу без перевірки, тим більше, що -1це дійсний індекс в Python. У C ваш покажчик NULL принаймні надасть вам сегмент за замовчуванням.

У PHP повертається особливе значення іншого типу: булеве значення, FALSEа не ціле число. Як виявляється, це насправді не є кращим завдяки неявним правилам перетворення мови (але зауважте, що і в Python булели можуть використовуватися як ints!). Функції, які не повертають послідовний тип, як правило, вважаються дуже заплутаними.

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

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

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

Привіт @SteveBarnes, приклад ділення на нуль помилок помилок звучить як погане програмування (із занадто загальним уловом, який не збільшує виняток).
wTyeRogers

Здається, ваша відповідь зводиться до: А) "НІ!" Б) є вагомі приклади, коли потік управління через винятки працює краще, ніж альтернативи С) виправлення коду питання для використання генераторів (які використовують винятки як контрольний потік, і роблять це добре). Хоча ваша відповідь, як правило, інформативна, в ній не існує аргументів на підтримку пункту A, який, будучи жирним і, здавалося б, основним твердженням, я би очікував певної підтримки. Якби це було підтримано, я б не спростував вашу відповідь. На жаль ...
wTyeRogers
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.