У темі 02 ми домовились,
що traceback — не привід засмучуватись, а звіт, який треба прочитати з кінця. Відтоді
ти бачив десятки таких звітів: NameError, коли одрукувався в імені,
TypeError, коли спробував помножити None,
KeyError, коли поліз у словник по ключ, якого там немає. Щоразу програма
зупинялась, і це було правильно.
Але не завжди зупинка — правильна поведінка. Уяви програму, яка читає прайс із тисячі рядків, і в 348-му рядку хтось написав ціну словами. Чи справді варто викинути всю роботу через один зіпсований рядок? Часто ні: розумніше пропустити його, записати в журнал і рахувати далі. Щоб так вміти, потрібен інструмент — і він у Python є. Ця тема про нього.
Наскрізний приклад — розбір текстового прайсу. Це той самий кошик, з яким ми працювали в темі 06, темі 12 і темі 14, тільки тепер він прийшов до нас у вигляді рядків тексту — а текст, на відміну від наших власних літералів, буває битим:
01 / РозрізненняДва роди помилок
Перше, що треба чітко розділити, — це дві принципово різні речі, які новачок часто звалює в купу під словом «помилка».
Синтаксична помилка (SyntaxError) — програма
не запустилась. Python читає файл цілком, перевіряє граматику й, якщо граматика
зламана, відмовляється виконувати навіть перший рядок. Не має значення, що помилка
в кінці файлу: перший print теж не спрацює.
Виняток під час виконання (exception) — програма запустилась і працювала, дійшла до якогось рядка й на ньому спіткнулась. Граматика бездоганна, але дія в цю конкретну мить неможлива.
Різницю видно навіть на око — саме про це була врізка в
темі 02. У синтаксичної помилки
немає шапки Traceback (most recent call last), бо шапка описує
маршрут виконання, а виконання не починалось. Побачив traceback із шапкою —
знай: програма таки бігла.
Практичний наслідок величезний. Синтаксичну помилку неможливо «обробити»: не існує способу написати код, який упорається з тим, що файл узагалі не прочитався. Її можна тільки виправити. А виняток під час виконання — можна перехопити, бо на момент його появи програма жива й здатна щось зробити.
float має правило:
«перетворюю рядок на число». Рядок "девʼятнадцять" — виняток із цього
правила, і функція повідомляє про нього єдиним способом, який має: припиняє нормальний
хід і кидає нагору спеціальний обʼєкт. Слово «помилка» вводить в оману, бо виняток —
не завжди чиясь провина. Кінець файлу, обірваний звʼязок, натиснутий Ctrl+C — це теж
винятки.02 / Основаtry і except
Конструкція, заради якої написана вся тема, складається з двох частин:
Читається дослівно: «спробуй зробити те, що в першому блоці;
якщо станеться ValueError — виконай другий блок замість
падіння». Механіка така:
- Python виконує тіло
tryрядок за рядком, як звичайний код. - Якщо все пройшло без винятків — блок
exceptпросто пропускається. Він не виконується ніколи, і жодного сліду від нього не лишається. - Якщо на якомусь рядку стався виняток — решта тіла
tryне виконується, а Python дивиться, чи підходить тип винятку під назву вexcept. Підходить — керування стрибає в блокexcept, і після нього програма спокійно йде далі. - Не підходить — виняток летить далі вгору так, наче
tryтут і не було.
Пункт 3 варто перечитати: «решта тіла try не виконується». Якщо
в try три рядки й упав другий, третій не спрацює. Тому в try
не пхають півпрограми — там має лежати рівно та дія, яка може впасти,
і бажано одна.
03 / КаркасІєрархія винятків
У except ValueError стоїть не чарівне слово, а імʼя типу.
Усі винятки в Python — обʼєкти, а всі їхні типи вишикувані в одне дерево спадкування.
Це і є ключ до всієї теми: except ловить не лише названий тип,
а весь піддерево під ним.
Корінь дерева — BaseException. Одразу під ним три особливі гілки
(SystemExit, KeyboardInterrupt, GeneratorExit)
і четверта, у якій живе все звичайне, — Exception. Ці три винесені
з-під Exception навмисно, і зараз ти побачиш навіщо.
Інтерактив 1 · Що саме зловить твій except
Обери тип у except — підсвітиться все, що він перехопить. Дивись на дві нижні гілки зліва вгорі.
except Exception
ловить 11 типів із 15 і свідомо не чіпає KeyboardInterrupt та
SystemExit — тому Ctrl+C зупиняє програму навіть усередині обробки.
except BaseException ловить усі 15, і Ctrl+C перестає працювати.
Саме тому перше пишуть часто, а друге — майже ніколи.Дерево пояснює речі, які інакше виглядають довільними:
except LookupErrorловить іIndexError(вийшов за межі списку), іKeyError(немає ключа в словнику) — бо обидва є його підтипами. Дві різні на вигляд біди мають спільного предка «щось не знайшлося за адресою».except OSErrorловитьFileNotFoundError,PermissionErrorі ще з десяток родичів — усе, що стосується розмови з операційною системою.except Exceptionловить майже все, що можна назвати «збоєм програми», але не ловить прохання користувача чи системи завершитись.
isinstance.
Коли Python вирішує, чи підходить except X до винятку, він питає рівно
одне: «чи є цей обʼєкт екземпляром X або будь-якого його підкласу».
Тому ZeroDivisionError ловиться і як ZeroDivisionError,
і як ArithmeticError, і як Exception, і як
BaseException — чотирма різними рядками, і всі чотири правильні.
Питання лише в тому, наскільки широко ти хочеш ловити.04 / АнтипатернЧому голий except — біда
Python дозволяє написати except: без жодного типу. Це синтаксично
законно й означає «злови взагалі все» — тобто те саме, що
except BaseException:. Виглядає зручно: одна конструкція на всі випадки
життя, ніколи нічого не впаде. Саме тому цей рядок і трапляється в чужому коді
регулярно.
Проблема в тому, що «ніколи нічого не впаде» — не мета. Мета — щоб програма
робила правильні речі. А голий except ковтає три категорії подій,
які ковтати не можна ніколи.
Інтерактив 2 · Той самий код із except: і з except ValueError:
Чотири ситуації. Перші дві — однакові. На третій і четвертій версії розходяться.
flot замість
float — це NameError, тобто помилка програміста.
Голий except перетворює її на охайне повідомлення «не вдалося
розібрати» й підставляє нуль. Програма працює далі, звіти сходяться, а числа
неправильні. Це найдорожчий сорт багу: без падіння й без сліду.Отже, три категорії, які голий except ковтає разом із корисним:
- Помилки в твоєму власному коді.
NameError,TypeError,AttributeError— це не «погані дані», це зламана програма. Її треба лагодити, а не приховувати. KeyboardInterrupt— Ctrl+C. Якщо він потрапляє в голийexceptусередині циклу, програму стає неможливо зупинити з клавіатури: кожне натискання ввічливо перехоплюється й ігнорується.SystemExit— те, що кидаєsys.exit(). Голийexceptскасовує запит на завершення, і програма, яка мала зупинитись, живе далі.
contextlib.suppress, і ми до нього ще дійдемо.05 / ПорядокКілька except і чому порядок важить
Одна дія може впасти по-різному, і на кожен випадок можна написати свою реакцію. Наш розбір рядка прайсу — саме такий:
Рядок "яблука;девʼятнадцять" дасть ValueError на
float. Рядок "мед" дасть IndexError ще раніше,
на частини[1]: після split у списку один елемент, а ми
просимо другий. Дві біди — дві гілки:
Python перебирає гілки згори вниз і зупиняється на першій, яка підходить. Виконується рівно одна — навіть якщо підійшли б дві. Звідси й головна пастка: якщо широкий тип поставити раніше за вузький, вузький ніколи не спрацює.
Інтерактив 3 · Порядок гілок вирішує все
Ті самі дві гілки, переставлені місцями. Перемикай тип винятку й дивись, яка спрацювала.
except ValueError не спрацьовує жодного разу з трьох:
широкий except Exception стоїть вище й забирає все. Python на це
не скаржиться — недосяжна гілка не є синтаксичною помилкою, тож знайти її можна
тільки очима.Правило просте: від конкретного до загального. Спершу вузькі типи з осмисленою реакцією, найширший — останнім, якщо він узагалі потрібен. Якщо ж дві різні біди мають однакову реакцію, їх можна злити в один рядок через кортеж:
Дужки тут обовʼязкові: без них Python зрозуміє запис інакше. Це звичайний кортеж типів, і перевірка проходить, якщо виняток підходить хоч під один із них.
06 / Повна формаelse і finally
У конструкції є ще дві частини, і обидві мають чітко визначену мить.
elseвиконується, якщо вtryне сталося винятку. Питання «навіщо, якщо можна просто дописати рядки в кінецьtry» має точну відповідь: щоб ці рядки не були під наглядом. Якщо покласти їх уtry, а вони теж кинутьValueError, їхня помилка потрапить у той самийexceptі замаскується під помилку розбору.finallyвиконується завжди. І коли все пройшло гладко, і коли виняток спіймали, і коли не спіймали й він летить далі, і навіть коли зtryвийшли черезreturn. Це місце для прибирання: закрити файл, віддати зʼєднання, зняти блокування.
Інтерактив 4 · Хто коли виконується
Чотири сценарії, крок за кроком. Стеж за рядком finally: він у виводі всюди.
else пропущено, а в третьому пропущено ще й except —
і все одно finally надрукувався перед тим, як виняток полетів угору.
Сценарій 4 — найтонший: return уже обчислив значення, але функція
ще не вийшла, доки finally не відпрацював.return у finally.
Якщо всередині finally написати return, він
переб'є все: і значення, яке готувався віддати try,
і виняток, який летів угору. Виняток мовчки зникне — програма отримає значення
з finally і навіть не дізнається, що щось падало. Це рідкісний випадок,
коли інструмент прибирання тихо ковтає аварію. Правило: у finally
прибирай, але не повертай.07 / ПолітКуди летить виняток
Тепер найважливіша механіка теми, і вона спирається просто на
стек викликів із теми 14. Нагадаю:
кожен виклик функції кладе на стопку свій кадр із параметрами й локальними іменами;
return знімає кадр і віддає значення туди, звідки викликали.
Виняток користується тим самим стеком, тільки в інший спосіб. Коли він виникає,
Python питає поточний кадр: «чи є тут try з відповідним
except?». Немає — кадр знищується, і те саме питання
дістається кадру нижче. І так далі, вниз по стопці, доки хтось не відповість «є».
Якщо не відповів ніхто, включно з найнижчим кадром <module>,
Python друкує traceback і завершує програму. І тепер зрозуміло, чому в traceback
кілька рядків «File …, line …, in …»: це список кадрів, які виняток пролетів
наскрізь, від найнижчого до того, у якому все почалось.
Інтерактив 5 · Виняток піднімається по стеку
Той самий стек кадрів, що в темі 14. Обери, де стоїть try, і пройди кроки.
try стоїть у
сума(), помирає один кадр і цикл продовжується — зіпсований рядок
пропущено, решта прайсу порахована. Коли try аж у
<module>, гинуть два кадри разом із недорахованою сумою.
Коли try немає ніде — гине вся програма. Один і той самий виняток,
три різні наслідки.Звідси випливає найважливіше практичне правило теми: лови виняток там, де
знаєш, що з ним робити. Не там, де він стався, і не там, де зручно, — а там,
де є достатньо контексту для рішення. Функція розібрати не знає, чи можна
пропустити рядок: може, це критично важливий прайс і краще впасти. А цикл, який читає
файл, знає — і саме йому вирішувати.
08 / Ініціативаraise: підняти виняток самому
Досі винятки кидав хтось інший — float, список, словник. Але кидати
може й твій власний код, і робити це варто частіше, ніж здається. Слово —
raise:
Замість IndexError із незрозумілим текстом ми тепер кидаємо
ValueError зі змістовним поясненням. Це та сама «рання відмова», що була
в темі 14, тільки замість повернення
спеціального значення функція чесно повідомляє: «так не можна».
Чому raise кращий за return None у ролі «щось не так»:
None легко проігнорувати — він мовчки поїде далі й вибухне рядків через
двадцять у зовсім іншому місці. Виняток проігнорувати неможливо: або ти його
обробляєш, або програма зупиняється тут і зараз, з точною адресою.
Ланцюжок: raise ... from ...
Часто виняток піднімають усередині except — щоб перевести
низькорівневу технічну біду в поняття своєї задачі. І тут постає питання: що робити
з первинним винятком, який ми щойно спіймали?
Інтерактив 6 · Три способи підняти виняток із except
Один і той самий код, три варіанти рядка raise — три різні traceback.
ValueError
у звіті немає взагалі.Практично: from e став звичкою хорошого тону — він коштує чотири
символи й зберігає повну картину. from None застосовують рідко й свідомо:
коли первинний виняток — суто внутрішня деталь, яка тільки заплутає того, хто читає
звіт.
Ще одна форма — raise без нічого всередині
except. Вона піднімає той самий виняток далі, зберігаючи його traceback
недоторканим. Класичне застосування — «записати в журнал і не приховувати»:
09 / ОбʼєктЩо всередині винятку
Виняток — це обʼєкт, і до нього можна дотягнутись через as:
Три речі, які варто знати:
str(e)— текст повідомлення, той самий, що стоїть у traceback після двокрапки. Саме його треба класти в свої повідомлення, а не вигадувати власне «щось пішло не так».e.args— кортеж аргументів, з якими виняток створили. Зазвичай там один рядок, але буває й більше: уOSError, наприклад, лежать і код помилки, і текст.- Імʼя
eживе тільки всередині блокуexcept. Після нього Python видаляє це імʼя — навмисно, щоб виняток не тримав у памʼяті весь стек кадрів, на які він посилається. Спроба звернутись доeнижче дастьNameError. Якщо значення потрібне далі — перепиши його в іншу змінну прямо в блоці.
KeyError в args[0] лежить сам відсутній ключ,
у FileNotFoundError є .filename і .errno,
у UnicodeDecodeError — точна позиція байта, на якому все зламалось. Це
рятує від розбору тексту повідомлення регулярними виразами: потрібне вже лежить полем.10 / ПідхідEAFP проти LBYL
Дві абревіатури, за якими стоять два способи писати той самий код.
LBYL — Look Before You Leap, «подивись, перш ніж стрибати». Спершу перевір умову, потім дій. Так пишуть у більшості мов, і так природно думає новачок.
EAFP — Easier to Ask Forgiveness than Permission, «легше попросити пробачення, ніж дозволу». Дій одразу, а якщо не вийшло — обробляй виняток. Це пітонівський стиль за замовчуванням, і в стандартній бібліотеці він скрізь.
Інтерактив 7 · Перевірити наперед чи спробувати
Одне завдання — взяти ціну зі словника, з нулем як запасним варіантом. Три ситуації.
Чесно про те, коли який доречний:
- EAFP краще, коли між перевіркою й дією може щось змінитись (файли, мережа, спільні дані), коли перевірка дублює роботу самої дії, і коли «поганий» випадок рідкісний.
- LBYL краще, коли перевірка дешева й однозначна, коли «поганих» випадків більшість, і коли ти перевіряєш не чужий стан, а свій власний аргумент — ту саму ранню відмову з теми 14.
І окремо: для словника є третій шлях, який знімає питання зовсім — метод
get зі теми 08:
ціни.get("мед", 0.0). Одне звернення, жодного винятку, жодної щілини.
Коли інструмент такий є, сперечатися про підхід не треба.
11 / Прибиранняwith у двох словах
Ти напевно бачив запис with open(...) as f:. Тепер зрозуміло, звідки
він узявся: це try/finally, спакований у два слова.
Обʼєкт, який уміє стояти після with, називають
менеджером контексту (context manager). Він знає дві речі: що зробити
на вході в блок і що зробити на виході — причому вихід відбувається завжди, як у
finally. Детально про файли й про те, як писати власні менеджери
контексту, буде в темі 20. Тут достатньо запамʼятати звʼязок:
with — це не магія, це finally з людським обличчям.
12 / СвоїВласний виняток
Коли задача має власні поняття, зручно мати й власні типи винятків. Рецепт складається з одного рядка:
Далі він поводиться як будь-який інший тип: raise ПомилкаПрайсу("рядок 348")
кидає, except ПомилкаПрайсу: ловить, а except Exception:
теж ловить — бо ми успадкували саме від нього.
class. Це синтаксис створення власного
типу, і повністю ми розберемо його в темі 25. Поки що прийми рядок
як рецепт: слово class, імʼя нового винятку, у дужках — від кого
успадковуємо, а в тілі досить докстрінга або pass. Успадковувати треба
від Exception, а не від BaseException — інакше твій виняток
проскочить повз усі чужі except Exception і поведеться як Ctrl+C.Навіщо це потрібно: власний тип дозволяє тому, хто користується твоїм кодом,
відрізнити «твоя функція каже, що дані погані» від «усередині твоєї функції щось
зламалось». Без власного типу обидві ситуації виглядають як ValueError,
і розрізнити їх можна хіба що за текстом повідомлення — а це погана опора.
13 / ГлибшеДля тих, хто вже пише код
Кілька речей, які не потрібні новачкові сьогодні, але потрібні на робочому проєкті.
Скільки коштує виняток
Питання не пусте, бо на ньому будується вибір між EAFP і LBYL у гарячому циклі. Порядки величин на CPython 3.12 такі: звернення до словника — десятки наносекунд; кинутий і спійманий виняток — приблизно мікросекунда, тобто в 15-30 разів дорожче. Звучить страшно, але дивись на добуток: якщо винятки трапляються в одному випадку зі ста, EAFP усе одно виграє, бо в решті девʼяноста девʼяти випадках він робить менше роботи.
І головне уточнення, яке багато хто пропустив: починаючи з Python 3.11
сам вхід у блок try коштує нуль. Раніше інтерпретатор
на кожному вході клав службовий запис на стек; тепер інформація про обробники лежить
у таблиці поруч із байткодом і потрібна лише в момент падіння. Тобто
try, у якому нічого не сталось, більше не сповільнює код взагалі —
аргумент «не обгортай цикл у try, це дорого» застарів.
contextlib.suppress
Якщо виняток справді треба проігнорувати — є чесний спосіб сказати це вголос:
Відмінність від except: pass не косметична. Тут названо
конкретний тип, тому будь-яка інша біда (немає прав, шлях — це тека)
полетить далі як належить. І намір видно з першого погляду: «цей виняток тут очікуваний
і нецікавий», а не «я не знаю, що тут може статись».
Логувати, а не друкувати
У бойовому коді замість print у блоці except пишуть
logging.exception("не вдалося розібрати рядок %s", рядок). Ця функція
сама додає до повідомлення повний traceback спійманого винятку — тобто ти отримуєш
і своє пояснення, і машинні подробиці. Половина розслідувань зводиться саме до
пошуку рядка, який хтось написав через print(e), загубивши стек.
Групи винятків
З Python 3.11 існує ExceptionGroup — обʼєкт, який несе
кілька винятків одночасно, і синтаксис except* для їх розбору.
Потрібно це там, де паралельно виконується багато задач і впасти може кілька відразу:
одна гілка except у старому сенсі тут просто не описує ситуацію. Для
послідовного коду це не знадобиться, але знати про існування варто — інакше
зірочка в чужому except* виглядає як одрук.
14 / ПідсумокЩо забрати з теми
- Синтаксична помилка й виняток — різні речі. Перша означає, що програма не запустилась, і обробити її неможливо. Другий стається на живій програмі — і його можна перехопити.
try/exceptловить за типом. Тип — це вузол у дереві спадкування, іexceptловить весь піддерево під ним.- Голий
except:— антипатерн. Він ковтає помилки твого коду, Ctrl+C іsys.exit().except Exceptionуже значно безпечніший, бо не чіпає останні два. - Гілки перебираються згори вниз. Спершу конкретні, найширша — остання. Широка гілка вище робить вузьку недосяжною, і Python про це не попереджає.
else— коли винятку не було,finally— завжди. Навіть коли виняток летить далі, навіть коли зtryвийшли черезreturn.- Виняток піднімається по стеку кадрів, знищуючи їх один за одним, доки не знайде обробника. Не знайде — програма завершується, а маршрут польоту і є traceback.
- Лови там, де знаєш, що робити. Не там, де впало.
raiseкидає,raise ... from eзберігає причину, голийraiseуexceptпередає той самий виняток далі.- EAFP — стиль за замовчуванням. LBYL доречний, коли перевірка дешева й стан не може змінитись між перевіркою й дією.
Тепер у тебе є все, щоб писати код, який не розсипається від першого поганого рядка.
Але лишилось питання, яке напрошується саме тут: а звідки ти знаєш, що твоя обробка
помилок узагалі працює? Що except IndexError справді спрацьовує на рядку
без крапки з комою, а finally справді закриває файл? Перевіряти це руками
щоразу — марна праця. Її автоматизують, і саме про це тема 19:
тестування з pytest, де рядок assert, який ти вже бачив
у практиці, виростає в повноцінний захист від власних майбутніх помилок.
practice.ipynb ти розбереш
той самий битий прайс: спершу без обробки й побачиш справжній traceback, потім
з конкретними гілками, потім із голим except — і власними очима
переконаєшся, як він приховує одрук у коді. Далі напишеш власний
ПомилкаПрайсу, порівняєш EAFP і LBYL на однакових даних і доведеш
через assert, що finally виконався навіть тоді, коли
виняток полетів далі.Далі в темі
Теорію прочитано. Тепер закріпи її на практиці.