Python raise и пользовательские исключения
Узнайте, как использовать оператор raise в Python, цепочки исключений raise...from и создавать собственные классы исключений для удобной обработки ошибок.
Python позволяет не только перехватывать ошибки — вы также можете сигнализировать о них явно с помощью оператора raise и создавать собственные типы исключений для представления предметно-специфических проблем. Эта глава дополняет раздел Python Try...Except и охватывает:
- Оператор
raise— вызов встроенных исключений - Повторный вызов исключений внутри блока
except - Цепочки исключений с
raise ... from - Создание пользовательских классов исключений
- Построение иерархии исключений для реального приложения
- Оператор
assertи случаи его применения
Оператор raise
Оператор raise позволяет вызвать исключение в любом месте вашего кода. Наиболее распространённая форма передаёт экземпляр исключения с описательным сообщением:
raise ExceptionType("message")Используйте raise, когда ваш код обнаруживает проблему, которую должен обработать вызывающий код. Например, функция, принимающая возраст, должна немедленно отклонять отрицательные значения, а не продолжать работу молча:
def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
return age
try:
set_age(-1)
except ValueError as e:
print(e)
# Output: Age cannot be negativeВыбор подходящего встроенного исключения
Встроенные типы исключений Python несут смысловую нагрузку. Правильный выбор исключения делает ваш API более понятным и позволяет вызывающему коду обрабатывать разные категории ошибок по отдельности.
| Исключение | Когда вызывать |
|---|---|
ValueError | Аргумент имеет правильный тип, но недопустимое значение (age = -1) |
TypeError | Аргумент имеет неправильный тип (age = "old") |
KeyError | Отсутствует обязательный ключ словаря |
IndexError | Индекс последовательности выходит за границы допустимого диапазона |
FileNotFoundError | Необходимый файл не существует |
PermissionError | Процесс не имеет прав для выполнения операции |
RuntimeError | Общая проблема во время выполнения, не подходящая под более конкретный тип |
NotImplementedError | Метод определён в базовом классе, но должен быть переопределён |
Вызов ValueError при неверном значении значительно информативнее, чем вызов голого Exception, поскольку вызывающий код может написать except ValueError для обработки именно этого случая.
Повторный вызов исключения
Иногда нужно выполнить какое-то действие при возникновении исключения — записать его в лог, освободить ресурс — а затем позволить тому же исключению распространиться к вызывающему коду без изменений. Вызовите raise без аргументов внутри блока except, чтобы повторно вызвать текущее исключение:
def read_config(path):
try:
with open(path) as f:
return f.read()
except FileNotFoundError:
print(f"Warning: config file not found at {path}")
raise # re-raise the original FileNotFoundError
try:
read_config("missing.cfg")
except FileNotFoundError as e:
print(f"Caught: {e}")
# Output:
# Warning: config file not found at missing.cfg
# Caught: [Errno 2] No such file or directory: 'missing.cfg'Голый raise сохраняет исходную трассировку стека, что делает отладку значительно проще, чем перехват и повторный вызов e в виде нового исключения.
Цепочки исключений с raise ... from
Когда вы перехватываете одно исключение и вызываете другое, Python автоматически записывает исходное исключение как контекст нового. Вы можете сделать эту связь явной и осмысленной с помощью raise NewException from original:
def load_data(path):
try:
with open(path) as f:
return f.read()
except OSError as e:
raise RuntimeError("Failed to load configuration") from e
try:
load_data("config.json")
except RuntimeError as e:
print(f"Error: {e}")
print(f"Caused by: {e.__cause__}")
# Output:
# Error: Failed to load configuration
# Caused by: [Errno 2] No such file or directory: 'config.json'При выводе трассировки стека Python отображает оба исключения по порядку, чётко показывая, что RuntimeError стал прямым следствием OSError. Это особенно полезно в библиотечном коде, где нужно преобразовывать низкоуровневые ошибки ОС в высокоуровневые доменные ошибки, не скрывая первопричину.
Подавление цепочки с raise ... from None
Иногда исходное исключение является деталью реализации, которую не нужно раскрывать. Передайте None в качестве причины, чтобы скрыть его:
def fetch(url):
try:
raise ConnectionError("timeout")
except ConnectionError:
raise RuntimeError("Network unavailable") from None
try:
fetch("http://example.com")
except RuntimeError as e:
print(f"Error: {e}")
print(f"Cause hidden: {e.__cause__}")
# Output:
# Error: Network unavailable
# Cause hidden: NoneВ трассировке будет показан только RuntimeError. Используйте этот приём редко — скрытие первопричины затрудняет отладку для пользователей библиотеки.
Создание пользовательских классов исключений
Встроенные исключения охватывают типичные программные ошибки, но слишком обобщены для доменных проблем. Если ваше приложение электронной коммерции вызывает обычный ValueError при сбое платежа, вызывающий код не может отличить это от неверного аргумента функции. Пользовательские классы исключений решают эту проблему.
Пользовательское исключение — это просто класс, наследующийся от Exception (или одного из его подклассов):
class InsufficientFundsError(Exception):
"""Raised when a bank account has insufficient funds."""
def __init__(self, amount, balance):
self.amount = amount
self.balance = balance
super().__init__(
f"Cannot withdraw {amount}: balance is only {balance}"
)
class BankAccount:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(amount, self.balance)
self.balance -= amount
return self.balance
account = BankAccount(100)
try:
account.withdraw(150)
except InsufficientFundsError as e:
print(e)
print(f"You tried to withdraw: {e.amount}")
print(f"Available balance: {e.balance}")
# Output:
# Cannot withdraw 150: balance is only 100
# You tried to withdraw: 150
# Available balance: 100Ключевые моменты этого паттерна:
super().__init__(message)задаёт удобочитаемую строку, возвращаемуюstr(e).- Дополнительные атрибуты (
self.amount,self.balance) позволяют вызывающему коду получать структурированные данные из исключения, а не только строку. - Чёткая строка документации описывает, когда должно вызываться исключение.
Построение иерархии исключений
В реальных приложениях часто бывает много связанных типов ошибок. Группировка их под общим базовым классом позволяет вызывающему коду перехватывать как конкретную ошибку, так и всю категорию целиком:
class AppError(Exception):
"""Base class for all application errors."""
class ValidationError(AppError):
"""Raised when user input fails validation."""
class DatabaseError(AppError):
"""Raised when a database operation fails."""
def validate_username(name):
if len(name) < 3:
raise ValidationError(f"Username '{name}' is too short (min 3 chars)")
try:
validate_username("ab")
except ValidationError as e:
print(f"Validation failed: {e}")
except AppError as e:
print(f"Application error: {e}")
# Output:
# Validation failed: Username 'ab' is too short (min 3 chars)Вызывающий код, которому нужно перехватывать только ошибки базы данных, может написать except DatabaseError. Вызывающий код, желающий перехватить любую проблему из вашей библиотеки, может написать except AppError. Это повторяет структуру собственной иерархии исключений Python, где OSError объединяет FileNotFoundError, PermissionError и несколько других.
Рекомендации по созданию пользовательских исключений
- Наследуйтесь от
Exception, а не отBaseException.BaseException— корень иерархии Python, включающий такжеSystemExitиKeyboardInterrupt, которые не должны перехватываться случайно. - Завершайте имя класса на
Errorдля исключений, сигнализирующих о проблеме. Это соответствует соглашениям Python (ValueError,TypeError,IOError). - Держите класс минимальным, если не нужны дополнительные атрибуты. Пустое тело со строкой документации совершенно допустимо.
- Помещайте исключения в отдельный модуль (например,
exceptions.py) в крупных проектах, чтобы вызывающий код мог импортировать их, не затрагивая остальной код.
Оператор assert
assert — это лёгкий способ выражать инварианты — условия, которые должны быть истинными для корректной работы вашего кода:
def divide(a, b):
assert b != 0, "Divisor must not be zero"
return a / b
try:
divide(10, 0)
except AssertionError as e:
print(f"AssertionError: {e}")
print(divide(10, 2))
# Output:
# AssertionError: Divisor must not be zero
# 5.0assert condition, message вызывает AssertionError с заданным сообщением, когда condition равно False.
Важное ограничение: Python удаляет операторы assert при запуске с флагом -O (оптимизация). Это означает:
- Используйте
assertтолько для проверки внутренней согласованности и вспомогательных средств отладки. - Используйте
raiseс подходящим исключением для проверки пользовательских входных данных и проверок публичного API, которые должны выполняться всегда.
Типичные ошибки
Перехват исключений с молчаливым игнорированием
# Bad — the error disappears
try:
result = risky_operation()
except Exception:
pass
# Better — at minimum, log or re-raise
try:
result = risky_operation()
except Exception as e:
print(f"Operation failed: {e}")
raiseВызов строки вместо исключения
# Wrong — strings are not exceptions
raise "something went wrong" # TypeError
# Correct
raise ValueError("something went wrong")Случайный перехват BaseException
# Dangerous — this catches KeyboardInterrupt and SystemExit too
except BaseException:
...
# Use Exception instead
except Exception:
...Итоги
| Техника | Когда использовать |
|---|---|
raise ExceptionType("msg") | Сигнализировать о проблеме, которую должен обработать вызывающий код |
raise (голый) | Повторно вызвать текущее исключение после логирования или очистки |
raise NewError(...) from original | Преобразовать низкоуровневую ошибку в высокоуровневую, сохраняя причину |
raise NewError(...) from None | Преобразовать ошибку, скрывая внутреннюю причину |
| Пользовательский класс исключения | Дать доменным ошибкам уникальный, перехватываемый тип |
| Иерархия исключений | Позволить вызывающему коду перехватывать узкие или широкие категории ошибок |
assert | Проверять внутренние инварианты только в процессе разработки |
Для полного понимания перехвата и обработки исключений см. Python Try...Except. Чтобы разобраться, как пользовательские исключения вписываются в проектирование классов, обратитесь к разделам Python Classes and Objects и Python Inheritance.