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

5.02. Исключения

Разработчику Архитектору

Исключения

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

В 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:, поскольку позволяет программе корректно реагировать на прерывания и завершения.

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

  1. Блок try-except
try:
risky_operation()
except SpecificError as e:
handle_error(e)

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

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

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

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

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

  1. Блок else.

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

try:
data = open_file()
except FileNotFoundError:
print("File not found")
else:
process(data) # только если файл успешно открыт
  1. Блок finally. Выполняется всегда, независимо от наличия исключения. Используется для освобождения ресурсов.
f = None
try:
f = open("file.txt")
process(f)
except IOError:
print("IO error")
finally:
if f:
f.close() # гарантированное закрытие

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

  1. Полная форма.
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 ValueError("Invalid value")
# или
exc = ValueError("Invalid")
raise exc

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

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

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

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

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

try:
int("abc")
except ValueError as e:
raise TypeError("Parsing failed") 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

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

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}")

Контекстные менеджеры и __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") # ошибка игнорируется

В асинхронном коде (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"Network error: {e}")
raise

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