Обработка исключений в 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 open — Lab — 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. Поэтому важно правильно управлять временем жизни задач.