Обработка и генерация исключений в Python: полный разбор try/except, raise, else/finally и пользовательских ошибок
Исключения в Python — это механизм сигнализации об ошибках во время выполнения. Правильная обработка исключений делает код надёжным и читаемым. Ниже — базовые конструкции try/except, связка else/finally, генерация ошибок raise, пользовательские исключения и практики.
Что такое исключения и почему они важны
Исключение — объект, который «выбрасывается» (raise) при ошибочной ситуации: деление на ноль, выход за пределы списка, сбой сети, неправильные данные. Если исключение не перехвачено, интерпретатор прерывает программу и печатает Traceback.
- Перехват исключений не скрывает ошибку, а делает поведение контролируемым.
- Обработка повышает устойчивость сервисов и CLI.
- Правильный перехват упрощает отладку.
Иерархия исключений в Python
Все исключения наследуются от BaseException. Для прикладного кода базовым родителем почти всегда является Exception. Ветвление важно для корректного перехвата: можно ловить как конкретные типы, так и целые группы.
# Фрагмент дерева (упрощённо)
BaseException
├─ SystemExit
├─ KeyboardInterrupt
└─ Exception
├─ ArithmeticError
│ ├─ ZeroDivisionError
│ └─ OverflowError
├─ LookupError
│ ├─ IndexError
│ └─ KeyError
├─ FileNotFoundError
├─ ValueError
├─ TypeError
├─ OSError
└─ RuntimeError
Рекомендация: перехватывайте конкретные исключения. Перехват «всего подряд» затрудняет диагностику и маскирует баги.
Базовая конструкция try/except
Используйте try/except, чтобы безопасно выполнить рискованный код:
def safe_div(x, y):
try:
return x / y
except ZeroDivisionError as e:
return float("inf") # или логика по умолчанию
Можно перехватывать несколько типов исключений разными блоками except или одной «кортежной» записью.
def parse_int(value):
try:
return int(value)
except (ValueError, TypeError) as e:
return None
Используйте as e, чтобы получить объект исключения и записать сообщение в лог.
Расширения: else и finally
Блок else выполняется, если в try не произошло исключения. Блок finally выполняется всегда — несмотря ни на что.
def read_number(path):
f = None
try:
f = open(path, "r", encoding="utf-8")
data = f.read()
except FileNotFoundError:
return None
else:
return int(data.strip())
finally:
if f:
f.close()
Чаще вместо ручного finally используйте with, который корректно освободит ресурсы.
Генерация исключений: raise
Когда входные данные некорректны или система в невалидном состоянии, выбрасывайте исключение явно с помощью raise. Это делает контракт функций прозрачным и предотвращает тихие сбои.
def sqrt_positive(x: float) -> float:
if x < 0:
raise ValueError('x must be non-negative')
return x ** 0.5
Для сохранения исходного контекста используйте raise from, чтобы связать исключения цепочкой (exception chaining) и быстрее диагностировать первопричину.
def load_json(path):
import json
try:
with open(path, encoding="utf-8") as f:
return json.load(f)
except OSError as e:
raise RuntimeError('Unable to read file') from e
Повторный выброс (re-raise) — просто raise без аргументов внутри блока except. Он полезен, если вы что-то залогировали и хотите передать ошибку выше.
try:
risky()
except Exception:
log_exception() # запись в журнал
raise
Пользовательские исключения
Создавайте собственные типы для доменных ошибок. Так код становится самодокументируемым, а вызывающая сторона может ловить ваши исключения отдельно от системных.
class PaymentError(Exception):
'Базовая ошибка платежей.'
class CardDeclinedError(PaymentError):
pass
def charge(amount, card):
if not card.valid:
raise CardDeclinedError('Card declined')
Совет: храните пользовательские исключения в модуле exceptions.py и документируйте условия их возникновения.
Множественные except и порядок проверки
Python сопоставляет исключение с блоками except сверху вниз. Узкие типы пишите раньше широких.
try:
...
except ValueError:
handle_value()
except Exception:
handle_generic()
EAFP против LBYL
Идиома Python — EAFP (Easier to Ask Forgiveness than Permission): «проще просить прощения, чем разрешения». Альтернатива — LBYL (Look Before You Leap): предварительно проверять условия.
# EAFP
try:
value = mapping[key]
except KeyError:
value = default
# LBYL
if key in mapping:
value = mapping[key]
else:
value = default
EAFP обычно короче и масштабируется лучше в конкурентных сценариях.
Практические паттерны обработки ошибок
- Валидация входных данных: выбрасывайте ValueError/TypeError для некорректного ввода.
- Работа с файлами: перехватывайте FileNotFoundError, PermissionError, используйте with.
- Сеть и API: ловите OSError, таймауты, ошибки библиотек; ретраи при нужде.
- Базы данных: разделяйте транзиентные и фатальные ошибки.
import json
from pathlib import Path
def load_settings(path: str) -> dict:
p = Path(path)
try:
text = p.read_text(encoding='utf-8')
return json.loads(text)
except FileNotFoundError:
return {'theme': 'light', 'autosave': False}
except json.JSONDecodeError as e:
raise ValueError(f'Invalid config format: {e}')
Логирование исключений
Используйте модуль logging вместо принтов. Метод logging.exception автоматически добавляет стек-трейс.
import logging
log = logging.getLogger(**name**)
def process(item):
try:
step(item)
except Exception:
log.exception('Failed to process item: %r', item)
raise
Короткий чек-лист лучших практик
- Ловите только те ошибки, с которыми реально можете что-то сделать.
- Сообщения делайте конкретными: указывайте проблемное значение, путь, ключ, лимит.
- Не используйте исключения как обычные if/else — это дорого и запутывает контроль потока.
- Для библиотек документируйте, какие типы ошибок кидаете и когда.
- В публичных API мапьте внутренние ошибки на доменные.
def fetch(user_id, repo):
if not isinstance(user_id, int):
raise TypeError('user_id must be int')
try:
return repo.get(user_id)
except OSError as e:
raise RuntimeError('storage error') from e
Анти-паттерны: чего избегать
- except Exception: без нужды — маскирует баги.
- голые except: перехватывают BaseException включая KeyboardInterrupt.
- молчаливый перехват: пустой обработчик без логирования.
- злоупотребление assert: утверждения могут быть отключены.
- неподходящие типы ошибок: ValueError для значения, TypeError для типа и т.п.
Исключения и контекстные менеджеры
Контекстные менеджеры инкапсулируют открытие/закрытие ресурсов и политику ошибок. Свой менеджер — через класс с __enter__/__exit__ или декоратор contextmanager.
from contextlib import contextmanager
@contextmanager
def opened(path, mode='r', encoding='utf-8'):
f = open(path, mode, encoding=encoding)
try:
yield f
finally:
f.close()
Исключения в тестах
В юнит-тестах проверяйте, что функция выбрасывает ожидаемое исключение. В pytest используйте raises.
import pytest
def test_sqrt_positive_raises():
with pytest.raises(ValueError):
sqrt_positive(-1)
Частые вопросы (FAQ)
1) В чём разница между except Exception и голым except?
except Exception перехватывает прикладные ошибки. Голый except перехватывает BaseException, включая KeyboardInterrupt и SystemExit. Практически всегда избегайте голого варианта.
2) Когда использовать else и finally?
else — когда нужно выполнить код, только если ошибок не было. finally — чтобы гарантированно освободить ресурсы независимо от успеха операции.
3) Что предпочесть: EAFP или LBYL?
В Python чаще выбирают EAFP — короче и безопаснее в условиях гонок. LBYL уместен, когда проверка дешёвая.
4) Как проектировать пользовательские исключения?
Создайте базовый класс доменных ошибок, наследуйте частные классы, документируйте причины и включайте полезный контекст в сообщения.
5) Можно ли логировать и при этом пробрасывать исключение дальше?
Да. Логируйте в except, затем делайте raise без аргументов — стек сохранится.
Помните: «Ловите то, что готовы обработать». Остальное должно падать быстро и громко 🧩
Освоив try/except, else/finally и raise, вы пишете предсказуемый код. Перехватывайте конкретные типы, логируйте исключения, проектируйте свои классы там, где это повышает ясность, и придерживайтесь идиомы EAFP. Удачной разработки! 💼
