Иерархия исключений, try/except/finally, пользовательские исключения
Правило: исключения — для исключительных ситуаций. Используйте их для ошибок и непредвиденных состояний, а не для обычного управления потоком.
BaseException
├── SystemExit # sys.exit() — выход из интерпретатора
├── KeyboardInterrupt # Ctrl+C
├── GeneratorExit # закрытие генератора
└── Exception # ← все обычные исключения наследуются отсюда
├── ArithmeticError
│ ├── ZeroDivisionError # деление на ноль
│ ├── OverflowError # число слишком большое
│ └── FloatingPointError
├── LookupError
│ ├── IndexError # индекс за пределами
│ └── KeyError # ключ не найден
├── ValueError # неверное значение (правильного типа)
├── TypeError # неверный тип
├── AttributeError # атрибут не найден
├── NameError # имя не определено
├── OSError # ошибки ОС (IO, файлы, сеть)
│ ├── FileNotFoundError
│ ├── PermissionError
│ └── TimeoutError
├── RuntimeError # общие ошибки выполнения
│ └── RecursionError
├── StopIteration # итератор исчерпан
├── ImportError # ошибка импорта
└── NotImplementedError # метод не реализован
except Exception: — ловит всё кроме SystemExit, KeyboardInterrupt, GeneratorExitexcept BaseException: — ловит абсолютно всё (очень редко нужно)except: — то же что except BaseException: — антипаттернtry / except / else / finallytry:
result = risky_operation()
except ZeroDivisionError as e:
print(f"Деление на ноль: {e}")
result = 0
except (TypeError, ValueError) as e:
print(f"Неверный тип или значение: {e}")
raise # перебрасываем исключение
else:
# Выполняется ТОЛЬКО если исключений не было
print(f"Успех: {result}")
save_result(result)
finally:
# Выполняется ВСЕГДА: и при успехе, и при исключении, и при return
cleanup()| Ситуация | except | else | finally |
|---|---|---|---|
| Нет исключений | ❌ | ✅ | ✅ |
| Исключение поймано | ✅ | ❌ | ✅ |
| Исключение не поймано | ❌ | ❌ | ✅ |
return в try | ❌ | ❌ | ✅ (до return!) |
def demo():
try:
return 1
finally:
print("finally!") # выведется перед возвратом 1
return 2 # перезаписывает return 1!
demo() # prints "finally!", returns 2Осторожно:
returnвfinallyперезаписываетreturnизtry. Не делайте это.
raise)# Создание нового исключения
raise ValueError("Значение не в допустимом диапазоне")
# С дополнительными атрибутами
raise ValueError("Ошибка", "field_name", 400)
# Перебросить текущее исключение (внутри except)
try:
risky()
except ValueError:
log_error()
raise # re-raise — сохраняет оригинальный traceback
# Raise None — тоже ре-рейз (редко нужно)
raiseraise ... from ...)try:
config = json.loads(config_text)
except json.JSONDecodeError as e:
raise ConfigError("Неверный формат конфига") from e
# Трейсбэк покажет ОБА исключения с пометкой "The above exception was the direct cause of"
# Подавить контекст (скрыть оригинальное исключение):
try:
...
except Exception:
raise NewError("Другая ошибка") from None
# Контекст подавлен — трейсбэк только для NewError# __cause__ — явная цепочка (raise ... from ...)
# __context__ — неявная цепочка (исключение в except блоке)
# __suppress_context__ — True если использован from None# Базовое исключение для модуля/домена
class AppError(Exception):
"""Базовое исключение приложения."""
pass
# Конкретные исключения с метаданными
class ValidationError(AppError):
def __init__(self, message: str, field: str = None, code: int = 400):
super().__init__(message)
self.field = field
self.code = code
def __str__(self):
if self.field:
return f"[{self.field}] {super().__str__()}"
return super().__str__()
class NotFoundError(AppError):
def __init__(self, resource: str, id: int):
super().__init__(f"{resource} with id={id} not found")
self.resource = resource
self.id = id
# Иерархия для API
class APIError(AppError):
status_code = 500
class AuthenticationError(APIError):
status_code = 401
class AuthorizationError(APIError):
status_code = 403
class RateLimitError(APIError):
status_code = 429
def __init__(self, retry_after: int):
super().__init__(f"Rate limit exceeded. Retry after {retry_after}s")
self.retry_after = retry_afterДва стиля обработки ошибок:
LBYL (Look Before You Leap) — проверяй перед использованием:
if key in d:
value = d[key]
process(value)EAFP (Easier to Ask Forgiveness than Permission) — Pythonic стиль:
try:
value = d[key]
process(value)
except KeyError:
pass| Критерий | LBYL | EAFP |
|---|---|---|
| Ошибки редки | Избыточная проверка | Оптимально |
| Ошибки часты | Оптимально | Много except-блоков |
| Race conditions | Возможны (проверил → изменилось) | Безопасен |
| Читаемость | Явные условия | Чище в happy path |
Общее правило: для I/O (файлы, сеть, БД) — EAFP. Для предсказуемых условий — LBYL.
import logging
import traceback
logger = logging.getLogger(__name__)
def process_safely(data):
try:
return heavy_computation(data)
except MemoryError:
logger.critical("Недостаточно памяти!", exc_info=True)
raise
except Exception as e:
# exc_info=True включает полный traceback в лог
logger.exception(f"Ошибка обработки: {e}")
# logger.exception эквивалентно logger.error(..., exc_info=True)
return None
# Получить traceback как строку:
try:
risky()
except Exception:
tb_str = traceback.format_exc()
send_to_monitoring(tb_str)contextlib.suppress — тихое подавлениеfrom contextlib import suppress
# Вместо:
try:
os.remove("temp.txt")
except FileNotFoundError:
pass
# Можно:
with suppress(FileNotFoundError):
os.remove("temp.txt")
# Подавляет только указанные типы — остальные пробрасываются
with suppress(KeyError, IndexError):
value = data["key"][0]warnings)import warnings
# Генерация предупреждений (не исключений)
def deprecated_function():
warnings.warn(
"deprecated_function устарела, используйте new_function()",
DeprecationWarning,
stacklevel=2 # указывает на вызывающий код, не на эту строку
)
# ...
# Фильтрация предупреждений:
warnings.filterwarnings("ignore", category=DeprecationWarning)
warnings.filterwarnings("error", category=RuntimeWarning) # превратить в исключение
# Временный фильтр:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
noisy_legacy_function()ExceptionGroup позволяет одновременно выбросить несколько исключений — полезно для asyncio Task Group и Trio.
# ExceptionGroup содержит список исключений
eg = ExceptionGroup("multiple errors", [
ValueError("ошибка 1"),
TypeError("ошибка 2"),
])
raise eg
# Ловить через except*:
try:
risky_concurrent_task()
except* ValueError as eg:
for exc in eg.exceptions:
print(f"ValueError: {exc}")
except* TypeError as eg:
for exc in eg.exceptions:
print(f"TypeError: {exc}")# ❌ Голый except — ловит всё, включая KeyboardInterrupt
try:
something()
except:
pass
# ❌ except Exception: pass — проглатывает ошибки молча
try:
critical_operation()
except Exception:
pass # никто не узнает об ошибке!
# ❌ Слишком широкий except — маскирует реальную проблему
try:
result = complex_logic()
except Exception as e:
return default # какая именно ошибка? неизвестно
# ✅ Конкретные типы + логирование + re-raise при необходимости
try:
result = complex_logic()
except SpecificError as e:
logger.error(f"Expected error: {e}")
return default
except UnexpectedError:
logger.exception("Unexpected error in complex_logic")
raise
# ❌ Использование исключений для обычного управления потоком
def get_first(lst):
try:
return lst[0]
except IndexError:
return None
# ✅ Явная проверка
def get_first(lst):
return lst[0] if lst else Noneimport time
import functools
from typing import Type
def retry(exceptions=(Exception,), max_attempts=3, delay=1.0, backoff=2.0):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
attempt = 0
current_delay = delay
while attempt < max_attempts:
try:
return func(*args, **kwargs)
except exceptions as e:
attempt += 1
if attempt >= max_attempts:
raise
print(f"Попытка {attempt} не удалась: {e}. Повтор через {current_delay}s")
time.sleep(current_delay)
current_delay *= backoff
return wrapper
return decorator
@retry(exceptions=(ConnectionError, TimeoutError), max_attempts=3, delay=0.5)
def fetch_data(url):
return requests.get(url).json()# Структура для большого проекта:
class DomainError(Exception):
"""Базовый класс для всех бизнес-исключений."""
class UserError(DomainError):
pass
class UserNotFoundError(UserError):
def __init__(self, user_id: int):
super().__init__(f"Пользователь {user_id} не найден")
self.user_id = user_id
class InsufficientBalanceError(UserError):
def __init__(self, balance: float, required: float):
super().__init__(f"Недостаточно средств: баланс {balance}, требуется {required}")
self.balance = balance
self.required = required
class PaymentError(DomainError):
pass
# Ловить всё из домена:
try:
process_payment(user_id, amount)
except UserError as e:
return 400, str(e)
except PaymentError as e:
return 402, str(e)
except DomainError as e:
return 500, "Внутренняя ошибка"tracebackimport traceback
import sys
try:
raise ValueError("test error")
except ValueError:
# Получить traceback как строку
tb_str = traceback.format_exc() # полный traceback
tb_lines = traceback.format_tb(sys.exc_info()[2]) # только TB (без типа/значения)
# Напечатать в stderr (стандартное поведение)
traceback.print_exc()
# Напечатать в файл или StringIO
import io
buf = io.StringIO()
traceback.print_exc(file=buf)
error_report = buf.getvalue()
# Форматирование без активного исключения:
try:
risky()
except Exception as e:
frames = traceback.extract_tb(e.__traceback__)
for frame in frames:
print(f"{frame.filename}:{frame.lineno} in {frame.name}")
print(f" {frame.line}")sys.exc_info() — тройка (type, value, traceback)import sys
def handle_any_exception():
exc_type, exc_val, exc_tb = sys.exc_info()
if exc_type is None:
return # нет активного исключения
# Полезно для обёрток и middleware где нет прямого доступа к `as e`
if issubclass(exc_type, ValueError):
logger.warning(f"Validation error: {exc_val}")
else:
logger.exception(f"Unexpected: {exc_val}")import pytest
def divide(a, b):
if b == 0:
raise ZeroDivisionError("division by zero")
return a / b
# pytest.raises как контекстный менеджер
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError) as exc_info:
divide(10, 0)
assert "division by zero" in str(exc_info.value)
assert exc_info.type is ZeroDivisionError
# pytest.raises как декоратор-проверка (Python 3.10+)
def test_raises_any():
with pytest.raises((ValueError, TypeError)):
raise ValueError("ok")
# Проверка match (regex):
with pytest.raises(ValueError, match=r"invalid.*value"):
raise ValueError("invalid input value")
# Проверка что исключение НЕ вызывается:
def test_no_exception():
result = divide(10, 2)
assert result == 5.0 # просто нет raises = исключение не вызваноfrom dataclasses import dataclass
from typing import TypeVar, Generic
T = TypeVar('T')
E = TypeVar('E', bound=Exception)
@dataclass
class Ok(Generic[T]):
value: T
def is_ok(self) -> bool: return True
def unwrap(self) -> T: return self.value
@dataclass
class Err(Generic[E]):
error: E
def is_ok(self) -> bool: return False
def unwrap(self): raise self.error
Result = Ok[T] | Err[Exception]
def safe_parse(s: str) -> Result:
try:
return Ok(int(s))
except ValueError as e:
return Err(e)
result = safe_parse("42")
if result.is_ok():
print(result.unwrap()) # 42
# Полезно для: API-responses, pipeline-операций, когда caller решает что делать с ошибкойsafe_divide(a, b) с EAFP-стилем и правильным логированием.retry(func, n=3, exceptions=(Exception,)) с экспоненциальным backoff.raise e vs raise внутри except Exception as e:.@handle_errors(default_value), возвращающий default при любом Exception.ExceptionGroup для сбора нескольких ошибок валидации полей формы и обработайте каждый тип отдельно через except*.Result[T, E] тип и перепишите через него функцию парсинга конфига.Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.