Exceptions
Exceptions - Винятки⚑
Виняток (Exception)⚑
Summary
Виняток (exception) - це помилки Python, виявлена під час виконання коду.
Обробка виняткових ситуацій або обробка винятків (exception handling) - механізм мов програмування, призначений для опису реакції програми на помилки під час виконання та інші можливі проблеми (винятки), які можуть виникнути під час виконання програми та призводити до неможливості (безглуздості) подальшого виконання основного алгоритму програми.
Код на Python може створити виняток за допомогою ключового слова raise. Після нього вказується об'єкт винятку. Також можна вказати клас винятку, в такому випадку буде автоматично викликаний конструктор без параметрів. raise може викидати в якості винятків лише екземпляри класу BaseException та його нащадків.
Блоки except обробляються зверху донизу, і управління передається не більш як одному обробнику. Тому, якщо потрібно обробити винятки, які знаходяться в ієрархії успадкування, по-різному, спочатку слід вказувати обробники менш загальних винятків, а потім - більш загальних. Також саме тому порожній except може бути лише останнім (інакше SyntaxError). Більш того, якщо спочатку розташувати обробники більш загальних винятків, то обробники менш загальних будуть просто проігноровані.
Як обробити виняток?⚑
Для обробки винятків у Python використовується конструкція try...except...finally.
try:
result = 10 / 0 # Code that may raise an exception
except ZeroDivisionError:
print("Error: Division by zero!") # Handling a specific type of exception
except Exception as e:
print("An error occurred:", e) # Handling other types of exceptions
else:
print("Division was successful!") # Executed if no exception occurred
finally:
print("Finishing the processing") # Executed regardless of whether an exception occurred
У цьому прикладі конструкція try...except дозволяє спробувати виконати код, в якому може виникнути виняток. Якщо виняток виникає, програма переходить до відповідного блоку except, який обробляє виняток певного типу. Якщо потрібно обробити винятки, які знаходяться в ієрархії успадкування, по-різному, спочатку слід вказувати обробники менш загальних винятків, а потім - більш загальних. У разі, якщо тип винятку не відповідає жодному блоку except, виняток може бути оброблений в блоку except Exception.
Блок else виконується, якщо виняток не виник, тобто код у try виконався успішно.
Блок finally виконується завжди, незалежно від того, чи виникла помилка чи ні. Він часто використовується для звільнення ресурсів або виконання завершальних операцій. Блок finally буде виконаний навіть якщо в блоці try є оператор return. Це одна з особливостей конструкції try...finally в Python. Навіть якщо в блоці try виконується return, блок finally все одно буде виконаний перед тим, як функція поверне значення. Це зручно використовувати для завершення сесії бази даних або закриття файлу.
Що станеться, якщо помилку не обробить блок except⚑
Якщо жоден із заданих блоків except не перехоплює виняток, то він буде перехоплений найближчим зовнішнім блоком try/except, в якому є відповідний обробник. Якщо ж програма не перехоплює виняток зовсім, то інтерпретатор завершує виконання програми і виводить інформацію про виняток до стандартного потоку помилок sys.stderr. Є два винятки з цього правила:
- Якщо виняток виникає в деструкторі об'єкта, виконання програми не завершується, а до стандартного потоку помилок виводиться попередження "Exception ignored" з інформацією про виняток.
- При виникненні винятку
SystemExitвідбувається лише завершення програми без виводу інформації про виняток на екран (не стосується попереднього пункту, в деструкторі поведінка цього винятку буде такою ж, як і у інших).
Що робити, якщо потрібно перехопити виняток, виконати дії і знову викликати цей самий виняток⚑
Для того, щоб в обробнику винятку виконати певні дії, а потім передати виняток далі на один рівень вище (тобто, знову викинути той самий виняток), використовується ключове слово raise без параметрів.
Для чого може використовуватися конструкція try...finally без except⚑
Якщо в блоці try виникне помилка, то блок finally все одно буде виконаний, і всередині нього можна зробити "cleanup" (наприклад).
Чи спрацює блок finally, якщо в try є return?⚑
Summary
Так.
finallyвиконується завжди - навіть якщо вtryєreturn,break,continueабо виняток. Python запам'ятовує значенняreturn, виконуєfinally, і лише потім повертає управління викликачу.
Як це працює:
- Python виконує
return "value"- запам'ятовує, що треба повернути"value". - Перед фактичним поверненням управління виконується блок
finally. - Тільки після цього викликачу повертається
"value".
Підводний камінь: якщо в finally теж є return - він переписує значення з try:
Так само поводиться з винятками - return у finally "поглине" виняток із try і функція поверне значення замість того, щоб виняток дійшов до викликача. Це майже завжди баг - не варто робити return усередині finally.
Що таке ланцюжок винятків⚑
У Python 3 при виникненні винятку в блоку except, попередній виняток зберігається в атрибуті __context__ і якщо новий виняток не обробляється, буде виведена інформація про те, що новий виняток виник при обробці попереднього ("During handling of the above exception, another exception occurred:").
Також можна зв'язувати винятки в один ланцюжок або замінювати старі новими. Для цього використовується конструкція raise новий_виняток from старий_виняток або raise новий_виняток from None.
У першому випадку вказаний виняток зберігається в атрибуті __cause__, а атрибут __suppress_context__ (який приховує виведення винятку з __context__) встановлюється в False. Тоді, якщо новий виняток не обробляється, буде виведена інформація про те, що старий виняток є причиною нового ("The above exception was the direct cause of the following exception:").
У другому випадку __suppress_context__ встановлюється в True, а __cause__ - в None. Тоді при виведенні винятку воно фактично буде замінено новим (хоча старий виняток все ще зберігається в __context__).
Для чого потрібен блок else⚑
Блок else виконується, якщо під час виконання блоку try не виникло винятків. Він призначений для відокремлення коду, який може спричинити виняток, який повинен бути оброблений в даному блоку try/except, від коду, який може спричинити виняток того ж класу, який повинен бути перехоплений на рівні вище, і мінімізує кількість операторів у блоці try.
Що робить contextlib.suppress⚑
contextlib.suppress — це контекстний менеджер, який використовується ігнорування певних винятків під час виконання блоку коду. Він зручний для випадків, коли потрібно просто ігнорувати певні винятки, не обробляючи їх додатково.
Використовується як більш явна альтернатива except: pass, оскільки він не повідомляє читачам коду, що ми насправді очікуємо, що цей виняток буде проігноровано.
contextlib.suppress приймає один або кілька типів винятків як аргументи та ігнорує їх, якщо вони виникають у відповідному блоці with, і виконання продовжується без помилки.
from contextlib import suppress
with suppress(FileNotFoundError): # Suppressing FileNotFoundError if the file does not exist
with open("non_existent_file.txt") as f:
content = f.read() # No error will be raised if the file is missing
Що можна передати у конструктор винятку⚑
Винятки можуть приймати будь-які безіменні аргументи у конструкторі. Вони розміщуються в атрибуті args у вигляді кортежу (не змінюваного списку). Найчастіше використовується один рядковий параметр, який містить повідомлення про помилку. У всіх винятках визначений метод __str__, який за замовчуванням викликає str(self.args).
Exception Object (Об'єкт винятку)⚑
arguments = <name>.args
exc_type = <name>.__class__
filename = <name>.__traceback__.tb_frame.f_code.co_filename
func_name = <name>.__traceback__.tb_frame.f_code.co_name
line = linecache.getline(filename, <name>.__traceback__.tb_lineno)
trace_str = ''.join(traceback.format_tb(<name>.__traceback__))
error_msg = ''.join(traceback.format_exception(exc_type, <name>, <name>.__traceback__))
Класи винятків⚑
BaseException- базовий клас для всіх винятків.- Системні винятки
SystemExit- виняток, який генерується функцієюsys.exit(). Використовується для завершення роботи програми.KeyboardInterrupt- завершення програми шляхом натисканняCtrl+Cв консолі.
Exception- клас-нащадокBaseException, базовий клас для всіх стандартних винятків, які не вказують на обов'язкове завершення програми, і для всіх користувацьких винятків.
Потомки Exception
SyntaxError- помилка синтаксису.IndentationError- неправильний відступ.TabError- змішане використання символів табуляції і пробілів.
ArithmeticError- базовий клас для всіх винятків, пов'язаних з арифметичними операціями.ZeroDivisionError- ділення на нуль.FloatingPointError- помилка операції над числами з рухомою комою.OverflowError- результат арифметичної операції занадто великий, щоб бути представленим.
AssertionError- невиконання умови в операторі assert.AttributeError- помилка доступу до атрибуту.EOFError- піднімається функцієюinput(), коли вона зустрічає умову завершення файлу (end-of-file condition).LookupError- базовий клас для винятків, коли в колекції не знайдено елемент.IndexError- неправильний індекс послідовності (наприклад, списку).KeyError- відсутній ключ у словнику
MemoryError- недостатньо пам'яті.NameError- ім'я не знайдено.UnboundLocalError- локальне ім'я використовується перед тим, як воно буде визначено.
OSError- системна помилка. Помилки, такі якFileExistsError/PermissionErrorRuntimeError- загальна помилка виконання, яка не належить до жодної з категорій.RecursionError- коли досягнута максимальна глибина рекурсії.NotImplementedError- дія не реалізована. Призначено, серед іншого, для створення абстрактних методів.
StopIteration- піднімається функцієюnext(), коли вона виконується на пустому ітераторі.TypeError- помилка несумісності типів даних, над якими виконується операція.ValueError- генерується, коли функції або операції передано об'єкт правильного типу, але з неправильним значенням, причому цю ситуацію неможливо описати більш точним винятком, таким як IndexError. Наприклад, спроба перетворити на число рядок, який не може бути числом.UnicodeError- генерується при невдалому кодуванні/декодуванні рядків в байти та навпаки.
BufferError- базовий клас для винятків, пов'язаних з операціями над буфером.ImportError- помилка імпорту модуля або імені з модуля.SystemError- некритична внутрішня помилка інтерпретатора. При виникненні цього винятка слід залишити звіт про помилку на сайті bugs.python.org
Виключення vs помилки компіляції⚑
Summary
Python виконується у дві фази: спочатку джерельний код компілюється у байткод, потім інтерпретатор виконує байткод. Помилки на першій фазі -
SyntaxErrorі похідні (IndentationError,TabError) - виникають до старту програми, ловити їх уtry/exceptу тому ж файлі неможливо. Виключення runtime-фази (ZeroDivisionError,KeyError,TypeError, ...) виникають у процесі виконання і обробляютьсяtry/except.
Дві фази виконання Python
source.py → [compile to bytecode] → source.pyc → [execute] → result
↑ ↑
SyntaxError here runtime exceptions here
(cannot be caught (can be caught
in same file) in try/except)
Етап компіляції в Python робиться інтерпретатором при запуску (або імпорті) і кешується у __pycache__/*.pyc. На цьому етапі парсер перевіряє синтаксис; якщо файл не парситься - інтерпретатор не починає виконувати жодного рядка з нього, тому try навколо некоректного коду в тому ж файлі не спрацює:
# This try doesn't help - SyntaxError happens at parse time,
# before any line of the file runs:
try:
x = 1 +
except SyntaxError:
print("caught")
Коли SyntaxError можна обробити
Випадки, коли парсинг виконується під час runtime і SyntaxError стає звичайним виключенням:
- імпорт модуля з помилкою синтаксису:
import broken_module; - виклик
eval(...)чиexec(...)з рядком джерельного коду; - компіляція через
compile(source, ...).
try:
exec("x = 1 +") # Parsed at runtime
except SyntaxError as e:
print(f"caught: {e}") # caught: invalid syntax
Чи має Python "компіляцію"?
Часто плутанина: "Python - інтерпретований, у нього немає компіляції". Точніше: Python компілює вихідний код у байткод (.pyc-файли в __pycache__), але без окремого кроку білду, який є у C/Java. Це не машинний код, а інструкції для віртуальної машини CPython. SyntaxError - результат саме цієї компіляції.
import dis
def f(x):
return x + 1
dis.dis(f)
# 2 0 RESUME 0
# 3 2 LOAD_FAST 0 (x)
# 4 LOAD_CONST 1 (1)
# 6 BINARY_OP 0 (+)
# 8 RETURN_VALUE
Споріднені compile-time-помилки
IndentationError- підкласSyntaxError, специфічний для неправильних відступів.TabError- підкласIndentationError, виникає при змішуванні табів і пробілів.NameErrorза використання змінної до присвоєння - це runtime-помилка, не compile-time. Компілятор бачить ім'я, але не перевіряє, чи воно ініціалізоване (Python - динамічна мова).
Links
В яких випадках можна обробити SyntaxError⚑
Помилка синтаксису виникає, коли синтаксичний аналізатор Python зіштовхується з частиною коду, який не відповідає специфікації мови і не може бути інтерпретований.
Оскільки у випадку синтаксичної помилки у головному модулі вона виникає до початку виконання програми і не може бути перехоплена, посібник для початківців у документації мови Python навіть розділяє синтаксичні помилки та виключення. Проте SyntaxError - це також виключення, яке успадковується від класу Exception, і є ситуації, коли воно може виникнути під час виконання та бути оброблене, а саме:
- помилка синтаксису в імпортованому модулі;
- помилка синтаксису в коді, який представляється рядком і передається функції
evalабоexec.
Чи можна створювати власні виключення⚑
Можна. Вони повинні успадковувати клас Exception. Зазвичай назви виключень закінчуються словом Error.
Для чого потрібні попередження (warnings) і як створити власне⚑
Попередження зазвичай виводяться на екран у випадках, коли не гарантується виникнення помилки і програма, як правило, може продовжувати роботу, але користувача слід повідомити про щось.
Базовим класом для попереджень є Warning, який успадковується від Exception. Базовим класом-нащадком Warning для користувацьких попереджень є UserWarning.
Для чого потрібний модуль warning⚑
У модулі warning зібрані функції для роботи з попередженнями.
Основною є функція warn, яка приймає один обов'язковий параметр message, який може бути або рядком-повідомленням, або екземпляром класу або підкласу Warning (у такому випадку параметр category встановлюється автоматично), а також два необов'язкових параметра: category (за замовчуванням - UserWarning) - клас попередження і stacklevel ( за замовчуванням - 1) - рівень вкладеності функцій, починаючи з якого необхідно виводити вміст стеку викликів (корисно, наприклад, для функцій-обгорток для виведення попереджень, де слід задати stacklevel=2, щоб попередження стосувалося місця виклику даної функції, а не самої функції).
Логування виключень⚑
Summary
Канонічний інструмент -
logger.exception(msg): логує повідомлення з рівнемERRORі автоматично долучає traceback з поточногоsys.exc_info(). Працює лише всерединіexcept-блоку. Поза ним -logger.error(msg, exc_info=True)абоlogger.error(msg, exc_info=exc). Використанняprint(e)абоprint(traceback.format_exc())припустимо лише в одноразових скриптах - у продакшен-коді трасування йде черезlogging.
logger.exception() - основний інструмент
import logging
logger = logging.getLogger(__name__)
def process(payload):
try:
result = parse(payload)
except ValueError:
logger.exception("Failed to parse payload id=%s", payload.id)
raise # Or return a default, depending on contract
Що це робить: - виставляє рівень ERROR; - додає exc_info з поточного активного виключення - traceback виводиться у лог-повідомленні; - не приховує помилку - raise без аргументів пере-кидає поточний виняток зі збереженим traceback.
Поза except-блоком
logger.exception() за межами except спрацює, але запише NoneType: None замість traceback. Якщо потрібно залогувати виключення, отримане як значення:
exc_info=exc (передається сам об'єкт виключення, не True) - валідно з Python 3.5.
Конфігурація форматера для повного контексту
За замовчуванням logging пише лише повідомлення. Для production-якісних логів потрібен форматер з timestamp, рівнем, модулем:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s [%(name)s:%(lineno)d] %(message)s",
)
Або через logging.config.dictConfig({...}) - для повного контролю над handler'ами, форматерами, фільтрами.
Структуроване логування
У сервісах із централізованим збором логів (ELK, Loki, Datadog) використовують structlog або python-json-logger - кожне повідомлення стає JSON-об'єктом з полями level, message, exception, request_id, user_id. Це дозволяє шукати/агрегувати помилки за полями замість regex по тексту.
import structlog
logger = structlog.get_logger()
try:
process(payload)
except ValueError:
logger.exception("parse_failed", payload_id=payload.id, kind="ValueError")
Типові помилки
logger.error(str(e))- друкує лише текст виключення, без traceback. У розслідуванні незрозуміло, де помилка виникла. Використовуватиlogger.exception()абоlogger.error(..., exc_info=True).except Exception: passз умовним логуваннямprint(e)- тиха втрата помилок. У продакшені такий код "успішно" продовжує роботу зі зламаним станом.except Exception: logger.exception(...)безraise- залежить від контракту. Якщо функція має повертати результат, а виняток замовчується - виклик отримаєNoneбез сигналу про проблему. Краще абоraise, або явно повернути sentinel.logger.error("err: " + str(e))замість format-параметрів -loggingformat-аргументи інтерполюються ліниво, лише якщо рівень увімкнено.logger.error("err: %s", e)- правильний патерн.
Sentry, Rollbar та інші trackers
Для production-сервісів виключення також відправляють у систему-трекер (Sentry, Rollbar). Інтеграція зазвичай через logging.Handler або middleware: будь-який logger.exception() автоматично створює event у Sentry.
Links