Перейти к основному содержимому

Обработка исключений в Python

Разработчику Архитектору
Сначала — общая теория

Ошибки, исключения и отказоустойчивость — что такое исключение как ожидаемое отклонение, цепочка raise … from, отличие от фатальных сбоев процесса.
Типы встроенных исключений — Распространённые типы исключений.


Исключения

Что такое исключение?

Исключения — это механизм, позволяющий обрабатывать ошибки и аномальные ситуации во время выполнения программы. В отличие от возврата кодов ошибок, исключения обеспечивают чистое разделение нормального потока выполнения и обработки сбоев, предотвращая распространение ошибок по стеку вызовов без явного управления.


Интерактивное демо — часть сценариев на Python (try / except); в Python синтаксис свой, но стек вызовов и раскрутка те же. Подробнее: ошибки и исключения.

Play ITЗагрузка интерактивного демо…

Теория — обработка исключений

Исключительная ситуация — состояние, при котором дальнейшая работа по основному алгоритму бессмысленна (нет файла, деление на ноль, исчерпана память). Язык передаёт управление вверх по стеку до подходящего except; без перехвата процесс завершается с трассировкой (крэш с точки зрения пользователя). Синхронные сбои возникают в известных операциях (open, /, индексация); асинхронные — сигналы ОС, обрыв питания.

В Python исключения — объекты-классы; перехват — структурный блок try / except / else / finally. Блок finally и контекстные менеджеры (with) соответствуют идее гарантированного завершения — очистка ресурса даже при сбое. Общая картина — ошибки и исключения; словарь терминов — Википедия: обработка исключений. Справочник типов — Распространённые типы исключений.


Как устроены исключения в Python

В Python используется модель исключений с раскруткой стека (stack unwinding) — когда исключение возникает, интерпретатор останавливает текущее выполнение, "раскручивает" стек в поисках блока except, и передаёт управление соответствующему обработчику. Если подходящий обработчик не найден — программа аварийно завершается с трассировкой стека (traceback).

Все исключения в Python — классы, наследующие от базового класса BaseException. Однако пользовательские и стандартные исключения, как правило, наследуются от Exception — подкласса BaseException.

BaseException
├── SystemExit # sys.exit()
├── KeyboardInterrupt # Ctrl+C
├── GeneratorExit # закрытие генератора
└── Exception # все остальные
├── StopIteration
├── ArithmeticError
│ ├── ZeroDivisionError
│ └── OverflowError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── ValueError
├── TypeError
├── NameError
├── AttributeError
├── OSError
│ ├── FileNotFoundError
│ ├── PermissionError
│ └── ...
├── ImportError
│ └── ModuleNotFoundError
└── ... (и многие другие)

SystemExit, KeyboardInterrupt, GeneratorExit — не должны перехватываться в общих except блоках, так как они управляют жизненным циклом программы.


Обработка исключений

Перехват всех исключений через except Exception — безопаснее, чем except BaseException:, поскольку позволяет программе корректно реагировать на прерывания и завершения.


Блок try-except

try:
risky_operation()
except SpecificError as e:
handle_error(e)

Разбор примера:

  • try выделяет участок с потенциальной ошибкой.
  • risky_operation() выполняется до исключения или завершения.
  • except SpecificError as e ловит конкретный тип исключения и сохраняет его в e.
  • handle_error(e) обрабатывает ошибку без аварийного завершения всего сценария.

Можно перехватывать несколько типов:

except (ValueError, TypeError) as e:
print(f"Invalid input: {e}")

Кортеж в except (...) объединяет несколько типов ошибок в один обработчик.

Или использовать отдельные блоки:

except ValueError:
print("Not a number")
except TypeError:
print("Wrong type")

Раздельные ветки удобны, когда для разных ошибок нужна разная реакция.

Порядок важен: более специфичные исключения должны идти первыми.


Блок else.

Выполняется, только если в try не было исключений.

try:
data = open_file()
except FileNotFoundError:
print("File not found")
else:
process(data) # только если файл успешно открыт

else запускается только при успешном try, поэтому в него выносят основной сценарий без смешивания с обработкой ошибок.


Блок finally.

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

f = None
try:
f = open("file.txt")
process(f)
except IOError:
print("IO error")
finally:
if f:
f.close() # гарантированное закрытие

finally выполняется всегда и гарантирует освобождение ресурса, даже если внутри try произошла ошибка.

Аналогичный, но более удобный способ — использование менеджеров контекста (with).


Полная форма.

try:
operation()
except SpecificError as e:
log(e)
raise # повторное возбуждение
except AnotherError:
fallback()
else:
commit()
finally:
cleanup()

Эта форма описывает полный жизненный цикл — попытка, обработка известных ошибок, ветка успеха и безусловная очистка.


Генерация исключений

Генерация исключений: raise.

Явное возбуждение исключения:

if x < 0:
raise ValueError("x must be non-negative")

raise вручную возбуждает исключение и принудительно переводит выполнение в аварийную ветку.

Можно передать экземпляр:

raise ValueError("Invalid value")
# или
exc = ValueError("Invalid")
raise exc

Повторное возбуждение (raise без аргументов).

Используется внутри except для передачи исключения дальше, сохраняя оригинальную трассировку:

except ValueError as e:
log_error(e)
raise # проброс с сохранением traceback

raise без аргументов повторно выбрасывает текущее исключение с исходным traceback.

Возбуждение с другим контекстом: raise ... from.

Позволяет указать причину (chained exception):

try:
int("abc")
except ValueError as e:
raise TypeError("Parsing failed") from e

from e связывает новую ошибку с первопричиной и делает диагностику прозрачной.

Результат:

Traceback (most recent call last):
File "...", line 3, in <module>
int("abc")
ValueError: invalid literal for int()

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "...", line 5, in <module>
raise TypeError("Parsing failed") from e
TypeError: Parsing failed

from None подавляет цепочку:

raise NewError("msg") from None

from None скрывает цепочку первичной ошибки, оставляя в выводе только итоговое исключение.

Для моделирования предметной области рекомендуется определять собственные иерархии исключений:

class AppError(Exception):
"""Базовое исключение приложения."""
pass

class ValidationError(AppError):
"""Ошибка валидации входных данных."""
def __init__(self, field, message):
super().__init__(f"Validation error in '{field}': {message}")
self.field = field

class AuthError(AppError):
"""Ошибка аутентификации."""
pass

Такая иерархия даёт единый базовый тип для ошибок приложения и отдельные подклассы для разных доменных ситуаций.

Использование:

if not token_valid(token):
raise AuthError("Token expired or invalid")

try:
validate_user(data)
except ValidationError as e:
print(f"Field {e.field} is invalid: {e}")

EAFP и LBYL

В Python принят стиль EAFP (Easier to Ask for Forgiveness than Permission): сначала пробуют операцию, при сбое обрабатывают исключение. Альтернатива — LBYL (Look Before You Leap): заранее проверять условие.

# EAFP — идиоматично для dict, файлов, парсинга
try:
value = config["timeout"]
except KeyError:
value = 30

# LBYL — уместно, когда проверка дешевле и яснее
if "timeout" in config:
value = config["timeout"]
else:
value = 30

Сравнение подходов:

  • EAFP выполняет действие сразу и ловит исключение, когда ошибка редкая.
  • LBYL проверяет условие заранее, когда это повышает читаемость и почти ничего не стоит.

Для доступа к ключам словаря dict.get() часто проще обоих вариантов. Для файлов и сети EAFP с try/except обычно читабельнее цепочек if os.path.exists(...). Примеры FileNotFoundError и with openLab — Python, файлы и текст.


Контекстные менеджеры и __exit__.

Обработка исключений тесно связана с протоколом контекстных менеджеров. Объект становится таковым, если реализует методы __enter__ и __exit__.

Метод __exit__(self, exc_type, exc_val, exc_tb) получает информацию об исключении:

  • exc_type — класс исключения (None, если нет).
  • exc_val — экземпляр исключения.
  • exc_tb — объект трассировки (traceback).

Если __exit__ возвращает True, исключение подавляется. В противном случае — продолжает распространяться.

class suppress:
def __init__(self, *exceptions):
self.exceptions = exceptions

def __enter__(self):
return self

def __exit__(self, exc_type, exc_val, tb):
return exc_type is not None and issubclass(exc_type, self.exceptions)

# Использование —
with suppress(FileNotFoundError):
os.remove("temp.tmp") # ошибка игнорируется

__exit__ получает сведения об исключении и, возвращая True, подавляет его распространение.

В асинхронном коде (async/await) исключения обрабатываются аналогично, но с учётом событийного цикла:

async def fetch():
try:
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
if resp.status != 200:
raise HttpError(resp.status)
return await resp.text()
except aiohttp.ClientError as e:
logger.error(f"Сеть error: {e}")
raise

В async-коде исключения часто возникают в точках await, поэтому обработчики ставят рядом с операциями ожидания.

Особенность: исключения в корутинах не возникают немедленно — они "запускаются" при await. Поэтому важно правильно управлять временем жизни задач.