Привет! Ты уже знаешь, как вручную вызывать исключения с помощью raise и использовать встроенные типы ошибок. Но что если ни одно встроенное исключение не описывает твою ситуацию? Например, в твоей программе может быть ошибка «Недостаточно средств на счете» или «Пользователь не найден». Для таких случаев Python позволяет создавать собственные исключения.
В этой статье мы разберём:
- Зачем создавать свои исключения
- Как создать класс исключения
- Как добавить атрибуты для полезной информации
- Как использовать свои исключения в коде
- Иерархия собственных исключений
- Что нужно знать перед началом
- Основная часть
- Зачем создавать свои исключения?
- Синтаксис создания исключений
- Простейшее собственное исключение
- Иерархия собственных исключений
- Добавление атрибутов в исключения
- Создание исключений для бизнес-логики
- Переопределение стандартных методов
- Использование с другими механизмами
- Связывание с вызывающим кодом
- Группировка исключений
- Полный пример: банковское приложение
- Задачи для закрепления
- Нюансы и подводные камни
- Класс исключения должен наследовать Exception
- Названия исключений
- Документация
- Частые ошибки и как их избежать
- Ошибка 1: Слишком общий except
- Ошибка 2: Забыл super() в init
- Ошибка 3: Создание исключения, но не вызов
- Шпаргалка
- Заключение
- КВИЗ
- Что дальше?
Что нужно знать перед началом
Для этого урока тебе понадобится:
- Установленный Python
- Базовое понимание классов в Python
- Знание исключений и конструкции
try-except
Совет: Создание своих исключений — это способ сделать код более выразительным и понятным. Когда ты читаешь
raise InsufficientFundsError, сразу понятно, что произошло.
Основная часть
Зачем создавать свои исключения?
Встроенные исключения хороши, но они слишком общие. ValueError может означать что угодно: отрицательный возраст, неправильный формат email, недостаточно средств — всё это ValueError. Но если ты создашь своё исключение:
raise InsufficientFundsError('На счете недостаточно средств')Ты сразу понимаешь, что произошло именно это, а не что-то другое.
Совет: Собственные исключения делают код самодокументируемым — они сами говорят о том, что случилось.
Синтаксис создания исключений
Создать своё исключение — это создать класс, который наследуется от Exception (или другого встроенного исключения).
class MyCustomError(Exception):
"""Моё собственное исключение."""
passСовет: Документируй свои исключения (docstring). Это помогает другим разработчикам понять, когда использовать это исключение.
Простейшее собственное исключение
class NegativeAgeError(Exception):
"""Исключение для отрицательного возраста."""
pass
def set_age(age):
if age < 0:
raise NegativeAgeError(f'Возраст не может быть отрицательным: {age}')
print(f'Возраст установлен: {age}')
# Использование
try:
set_age(-5)
except NegativeAgeError as e:
print(f'Ошибка: {e}')Результат:
Ошибка: Возраст не может быть отрицательным: -5Совет: Названия своих исключений принято заканчивать на
ErrorилиException.
Иерархия собственных исключений
Можно создавать целую иерархию исключений для разных типов ошибок.
class ValidationError(Exception):
"""Базовое исключение для ошибок валидации."""
pass
class NegativeAgeError(ValidationError):
"""Ошибка: возраст отрицательный."""
pass
class InvalidEmailError(ValidationError):
"""Ошибка: невалидный email."""
pass
class PasswordTooShortError(ValidationError):
"""Ошибка: пароль слишком короткий."""
passИспользование иерархии
def validate_user(name, age, email, password):
if not name:
raise ValidationError('Имя не может быть пустым')
if age < 0:
raise NegativeAgeError(f'Возраст {age} отрицательный')
if '@' not in email:
raise InvalidEmailError(f'Email {email} невалидный')
if len(password) < 6:
raise PasswordTooShortError('Пароль должен быть минимум 6 символов')
print('Пользователь валиден')
# Можно перехватывать все ошибки валидации
try:
validate_user('Анна', -5, 'anna@mail.ru', '12345')
except ValidationError as e:
print(f'Ошибка валидации: {e}')Результат:
Ошибка валидации: Возраст -5 отрицательныйСовет: Иерархия позволяет перехватывать все ошибки одного типа (
ValidationError) или только конкретные (NegativeAgeError).
Добавление атрибутов в исключения
Иногда нужно передать больше информации об ошибке: код ошибки, поле, в котором произошла ошибка, или исходные данные.
class ValidationError(Exception):
def __init__(self, message, field=None, code=None):
super().__init__(message)
self.field = field
self.code = code
def validate_age(age):
if age < 0:
raise ValidationError(
'Возраст не может быть отрицательным',
field='age',
code='NEGATIVE_AGE'
)
if age > 150:
raise ValidationError(
'Нереалистичный возраст',
field='age',
code='UNREALISTIC_AGE'
)
print(f'Возраст {age} валиден')
try:
validate_age(-10)
except ValidationError as e:
print(f'Ошибка: {e}')
print(f'Поле: {e.field}')
print(f'Код: {e.code}')Результат:
Ошибка: Возраст не может быть отрицательным
Поле: age
Код: NEGATIVE_AGEСовет: Атрибуты
fieldиcodeочень полезны для логирования и для фронтенда, где нужно показать ошибку рядом с конкретным полем.
Создание исключений для бизнес-логики
class AccountError(Exception):
"""Базовое исключение для операций со счётом."""
pass
class InsufficientFundsError(AccountError):
"""Ошибка: недостаточно средств."""
def __init__(self, balance, amount):
super().__init__(f'Недостаточно средств: баланс {balance}, запрошено {amount}')
self.balance = balance
self.amount = amount
class AccountNotFoundError(AccountError):
"""Ошибка: аккаунт не найден."""
def __init__(self, account_id):
super().__init__(f'Аккаунт {account_id} не найден')
self.account_id = account_id
class BankAccount:
def __init__(self, account_id, balance=0):
self.account_id = account_id
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
print(f'Снято {amount}. Остаток: {self.balance}')
# Использование
accounts = {1: BankAccount(1, 100)}
def get_account(account_id):
if account_id not in accounts:
raise AccountNotFoundError(account_id)
return accounts[account_id]
try:
account = get_account(2)
account.withdraw(200)
except AccountNotFoundError as e:
print(f'Ошибка: {e}')
except InsufficientFundsError as e:
print(f'Ошибка: {e}')Переопределение стандартных методов
Можно переопределить __str__ или __repr__ для более красивой строки ошибки.
class MyError(Exception):
def __init__(self, message, code):
self.message = message
self.code = code
def __str__(self):
return f'[{self.code}] {self.message}'
def __repr__(self):
return f'MyError(code={self.code}, message={self.message})'
try:
raise MyError('Что-то пошло не так', 404)
except MyError as e:
print(e) # [404] Что-то пошло не так
print(repr(e)) # MyError(code=404, message=Что-то пошло не так)Совет: Переопределяй
__str__для удобного вывода сообщения. Это полезно для логирования.
Использование с другими механизмами
Связывание с вызывающим кодом
class CustomError(Exception):
pass
def process_data(data):
try:
# Какой-то код с ошибкой
result = int(data)
except ValueError as e:
# Связываем с нашим исключением
raise CustomError('Не удалось преобразовать данные') from e
try:
process_data('abc')
except CustomError as e:
print(f'Ошибка: {e}')
print(f'Причина: {e.__cause__}')Результат:
Ошибка: Не удалось преобразовать данные
Причина: invalid literal for int() with base 10: 'abc'Группировка исключений
try:
# Код, который может вызвать разные ошибки
validate_user(name, age, email, password)
except (ValidationError, ValueError) as e:
print(f'Ошибка ввода: {e}')Полный пример: банковское приложение
import logging
class BankError(Exception):
"""Базовое исключение банка."""
pass
class InsufficientFundsError(BankError):
def __init__(self, balance, amount):
super().__init__(f'Недостаточно средств: баланс {balance}, запрошено {amount}')
self.balance = balance
self.amount = amount
class AccountNotFoundError(BankError):
def __init__(self, account_id):
super().__init__(f'Аккаунт {account_id} не найден')
self.account_id = account_id
class InvalidTransactionError(BankError):
def __init__(self, message, transaction_id):
super().__init__(message)
self.transaction_id = transaction_id
class BankAccount:
def __init__(self, account_id, balance=0):
self.account_id = account_id
self.balance = balance
def deposit(self, amount):
if amount <= 0:
raise InvalidTransactionError('Сумма депозита должна быть положительной', 'deposit')
self.balance += amount
print(f'Внесено {amount}. Баланс: {self.balance}')
def withdraw(self, amount):
if amount <= 0:
raise InvalidTransactionError('Сумма снятия должна быть положительной', 'withdraw')
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
print(f'Снято {amount}. Баланс: {self.balance}')
# Логирование ошибок
logging.basicConfig(level=logging.ERROR)
def handle_bank_operation():
accounts = {'001': BankAccount('001', 100)}
try:
account = accounts['002'] # Ошибка!
account.withdraw(200)
except AccountNotFoundError as e:
logging.error(f'Аккаунт не найден: {e}')
except InsufficientFundsError as e:
logging.error(f'Недостаточно средств: {e}')
except InvalidTransactionError as e:
logging.error(f'Ошибка транзакции {e.transaction_id}: {e}')
except BankError as e:
logging.error(f'Общая банковская ошибка: {e}')
except Exception as e:
logging.error(f'Неизвестная ошибка: {e}')
handle_bank_operation()Задачи для закрепления
Задача 1. Создай класс исключения NegativeValueError, который выбрасывается при отрицательном значении.
Задача 2. Создай исключение UserNotFoundError с атрибутом user_id.
Задача 3. Создай иерархию исключений для валидации: ValidationError → EmptyFieldError и InvalidFormatError.
Задача 4. Добавь собственное исключение в функцию divide(a, b) для деления на ноль.
Задача 5. Используй raise ... from для связывания своего исключения с встроенным.
Ответы:
Задача 1.
class NegativeValueError(Exception):
pass
def check_value(x):
if x < 0:
raise NegativeValueError(f'Значение {x} отрицательное')Задача 2.
class UserNotFoundError(Exception):
def __init__(self, user_id):
super().__init__(f'Пользователь {user_id} не найден')
self.user_id = user_id
def get_user(user_id):
# ... если не найден ...
raise UserNotFoundError(user_id)Задача 3.
class ValidationError(Exception):
pass
class EmptyFieldError(ValidationError):
def __init__(self, field):
super().__init__(f'Поле {field} пустое')
self.field = field
class InvalidFormatError(ValidationError):
def __init__(self, field, format_example):
super().__init__(f'Поле {field} имеет неверный формат. Пример: {format_example}')
self.field = field
self.format_example = format_exampleЗадача 4.
class DivisionByZeroError(Exception):
pass
def divide(a, b):
if b == 0:
raise DivisionByZeroError('На ноль делить нельзя')
return a / bЗадача 5.
class MyError(Exception):
pass
try:
int('abc')
except ValueError as e:
raise MyError('Ошибка преобразования') from eНюансы и подводные камни
Класс исключения должен наследовать Exception
# Неправильно
class MyError:
pass
# Правильно
class MyError(Exception):
passНазвания исключений
Принято использовать суффиксы Error или Exception для наглядности.
Документация
Всегда добавляй docstring к своему классу исключения — это помогает другим разработчикам.
Частые ошибки и как их избежать
Ошибка 1: Слишком общий except
# Перехватываем все ошибки
except Exception as e:
print(e)
# Перехватываем конкретное исключение
except MyCustomError as e:
print(f'Ошибка: {e}')Ошибка 2: Забыл super() в init
class MyError(Exception):
def __init__(self, message, code):
self.code = code
# Забыли super()
# Должно быть
super().__init__(message)
self.code = codeОшибка 3: Создание исключения, но не вызов
# Неправильно
try:
MyError('Ошибка')
except MyError:
pass
# Правильно
try:
raise MyError('Ошибка')
except MyError:
passШпаргалка
| Что нужно | Как пишется |
|---|---|
| Простейшее исключение | class MyError(Exception): pass |
| С атрибутами | class MyError(Exception): def __init__(self, msg): super().__init__(msg); self.extra = extra |
| Иерархия | class ChildError(ParentError): pass |
| Поднять исключение | raise MyError('Ошибка') |
| Перехватить | except MyError as e: |
| Связать с причиной | raise MyError('...') from e |
Заключение
Сегодня мы научились:
- Зачем создавать свои исключения
- Как создать класс исключения
- Создавать иерархии исключений
- Добавлять атрибуты для полезной информации
- Связывать исключения с помощью
from






