Исключения C: raise — выбрасываем ошибку сами
- Самим выбрасывать исключение с помощью
raiseс понятным сообщением - Выбирать подходящий тип ошибки — ValueError, TypeError и другие
- Понимать разделение работы: функция обнаруживает проблему и выбрасывает ошибку, вызывающий код решает, как её обработать
- Передавать ошибку дальше изнутри
exceptс помощью одногоraise
Что мы изучим?
До сих пор ошибки выбрасывал Python, а мы их ловили. Сегодня научимся выбрасывать их сами — с помощью raise, когда наш код обнаруживает недопустимое состояние.
Зачем это нужно?
Допустим, вы написали функцию withdraw(balance, amount) для снятия денег с банковского счёта. Что она должна делать, когда её просят снять минус 500? Варианты:
- Молча выполнить — баланс вырастет на 500. Серьёзный денежный баг.
- Напечатать «ошибка» — но тот, кто вызвал функцию, не знает, что она не сработала, и продолжает работать с неверным результатом.
- Вернуть специальное значение (например,
-1илиNone) — вызывающий код должен не забыть его проверить. Если забудет, баг «просочится». - Выбросить ошибку — вызывающий код обязан с ней разобраться, иначе программа остановится с понятным сообщением ровно в проблемном месте.
Вариант 4 — профессиональный способ. В теме ООП мы будем использовать его для проверки данных (Validation) внутри классов, а в темах бэкенда и баз данных сервер FastAPI будет превращать такие ошибки в HTTP-ответ 400 или 404.
Объяснение с основ
raise
raise выбрасывает исключение — точно так же, как это делает Python, когда что-то ломается. С момента raise:
- Функция немедленно останавливается. Ни одна строка после неё не выполняется, и значение не возвращается.
- Ошибка «поднимается» к тому, кто вызвал функцию. Если он ловит её через
except— отлично. Если нет, она поднимается дальше, и в конце концов программа падает с Traceback.
Правильное разделение: кто обнаруживает и кто обрабатывает
| Роль | Кто | Что делает |
|---|---|---|
| Обнаруживает проблему | Функция, которая получает данные | Проверяет и выбрасывает (raise) |
| Решает, что делать | Тот, кто вызвал функцию | Ловит (except) и решает: сообщение, ещё одна попытка, значение по умолчанию |
Официант (вызывающий код) просит кладовщика (функцию) 5 помидоров. Помидоров нет. Хороший кладовщик не приносит молча 5 яблок (неверное значение) и не «решает» сам поменять блюдо. Он объявляет (raise): «Помидоров нет!». А официант, который знает клиента, решает: предложить другое блюдо или извиниться. Каждый отвечает за свою область.
Какой тип ошибки выбрасывать?
Используйте встроенные типы по смыслу:
| Тип | Когда |
|---|---|
ValueError | Тип правильный, но значение недопустимо: отрицательный возраст, нулевая сумма, email без @ |
TypeError | Неверный тип: получен текст вместо числа |
KeyError / LookupError | Ищем то, чего не существует |
PermissionError | Нет права на действие |
Сообщение должно объяснять, что не так и почему: "amount must be positive, got -500", а не "error".
(В теме ООП, после того как изучим классы, увидим, как создавать собственные типы ошибок, например InsufficientFundsError.)
Синтаксис
raise ValueError("clear message about what is wrong")
# typical pattern: validate at the start of a function
def function_name(value):
if not is_valid(value):
raise ValueError("why it is invalid")
# ... normal work, only with valid data
# the caller decides
try:
function_name(bad_value)
except ValueError as error:
print("Problem:", error)
# re-raise: handle partly, then pass the same error up
except ValueError:
print("logging the problem...")
raiseПервый пример кода
week-2/withdraw.py:
def withdraw(balance, amount):
if amount <= 0:
raise ValueError("amount must be positive, got " + str(amount))
if amount > balance:
raise ValueError("insufficient funds: balance " + str(balance) + ", requested " + str(amount))
return balance - amount
print(withdraw(1000, 300))
print(withdraw(1000, -500))
print("this line never runs")Вывод:
700
Traceback (most recent call last):
File "withdraw.py", line 10, in <module>
print(withdraw(1000, -500))
File "withdraw.py", line 3, in withdraw
raise ValueError("amount must be positive, got " + str(amount))
ValueError: amount must be positive, got -500Разбор кода построчно
| Строка | Объяснение |
|---|---|
if amount <= 0: | Проверка данных (Validation): проверить, прежде чем что-либо делать. |
raise ValueError("...") | Создать объект ValueError с сообщением и выбросить его. Функция останавливается здесь. |
if amount > balance: | Вторая проверка — для другого бизнес-правила. |
return balance - amount | Сюда мы попадаем, только если все проверки пройдены. Дальше данные безопасны. |
withdraw(1000, 300) | Проходит проверки и возвращает 700. |
withdraw(1000, -500) | Выбрасывается ошибка. Никто её не ловит, поэтому программа падает с понятным Traceback. Последняя строка кода не выполняется. |
Обратите внимание на качество Traceback: сообщение говорит точно, в чём проблема (amount must be positive) и какое значение её вызвало (-500). Так выглядит хорошая ошибка.
Ещё один пример
Решает вызывающий код. Та же функция с кодом, который ловит и обрабатывает. week-2/atm_safe.py:
def withdraw(balance, amount):
if amount <= 0:
raise ValueError("amount must be positive, got " + str(amount))
if amount > balance:
raise ValueError("insufficient funds: balance " + str(balance) + ", requested " + str(amount))
return balance - amount
balance = 1000
requests = [200, -50, 5000, 300]
for amount in requests:
try:
balance = withdraw(balance, amount)
except ValueError as error:
print("Rejected:", error)
else:
print("Withdrew", amount, "- new balance:", balance)
print("Final balance:", balance)Вывод:
Withdrew 200 - new balance: 800
Rejected: amount must be positive, got -50
Rejected: insufficient funds: balance 800, requested 5000
Withdrew 300 - new balance: 500
Final balance: 500Разбор:
withdrawничего не печатает и не знает, что будет дальше. Она только обнаруживает и выбрасывает.- Цикл (вызывающий код) решает: запрос отклоняется с сообщением, и цикл переходит к следующему запросу.
- Когда ошибка выброшена, присваивание
balance = withdraw(...)не выполняется, поэтому баланс остаётся прежним. Именно этого мы и хотели. - Завтра ту же
withdrawможно использовать на сайте, в приложении или на сервере, и каждый из них обработает ошибку по-своему (сообщение на экране, HTTP-ответ400…).
raise без ничего после — передаём дальше
Иногда при ошибке нужно сделать что-то (например, записать её), но не «заставить её исчезнуть»:
try:
balance = withdraw(balance, 5000)
except ValueError:
print("Audit: a failed withdrawal attempt was recorded")
raiseОдиночный raise внутри except снова выбрасывает ту же самую ошибку. Она продолжает подниматься к тому, кто выше.
Типичная ошибка
Ошибка 1: print вместо raise.
def withdraw(balance, amount):
if amount > balance:
print("Not enough money!")
return balance - amountФункция печатает сообщение, но продолжает и возвращает отрицательный баланс! Вызывающий код не знает, что что-то пошло не так. К тому же печать «застревает» на экране и ничего не значит на сервере или в приложении.
Ошибка 2: бесполезное сообщение.
raise ValueError("error")
raise ValueError("bad input")Какой ввод? Какое было значение? Что ожидается? Хорошее сообщение = каково условие + что получено: "age must be between 0 and 120, got 150".
Ошибка 3: выбрасывать самый общий тип.
raise Exception("amount must be positive")Exception — родитель почти всех ошибок. Тому, кто хочет поймать только эту проблему, понадобится except Exception, который ловит и посторонние баги. Выбирайте конкретный тип (ValueError).
Ошибка 4: поймать и выбросить, потеряв контекст.
except ValueError:
raise ValueError("something failed")Исходное сообщение (с деталями) потеряно. Если хотите передать ошибку дальше, одиночный raise сохраняет исходную ошибку.
Практика
Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.
Упражнение 1: Допустимый возраст
Напишите функцию set_age(age), которая возвращает возраст, если он от 0 до 120, а иначе выбрасывает ValueError с сообщением, показанным в ожидаемом выводе (включая полученный возраст). Затем напишите цикл, который пробует возрасты [25, -3, 150]: для допустимого возраста печатает ok: и возраст, а при ошибке — error: и сообщение.
Ожидаемый вывод:
ok: 25
error: age must be between 0 and 120, got -3
error: age must be between 0 and 120, got 150Показать решение
MIN_AGE = 0
MAX_AGE = 120
def set_age(age):
if age < MIN_AGE or age > MAX_AGE:
raise ValueError("age must be between 0 and 120, got " + str(age))
return age
for age in [25, -3, 150]:
try:
print("ok:", set_age(age))
except ValueError as error:
print("error:", error)ok: 25
error: age must be between 0 and 120, got -3
error: age must be between 0 and 120, got 150Функция только обнаруживает проблему и выбрасывает ошибку. Вызывающий её код — цикл — решает, что делать.
Упражнение 2: Имя пользователя
Напишите функцию validate_username(name), которая выбрасывает ValueError:
- если имя короче 3 символов — с сообщением
too short; - если в нём есть пробел — с сообщением
no spaces allowed.
Если имя допустимо — возвращает его.
for name in ["dana", "al", "big boss"]:
try:
print("valid:", validate_username(name))
except ValueError as error:
print(name, "->", error)Ожидаемый вывод:
valid: dana
al -> too short
big boss -> no spaces allowedПоказать решение
MIN_USERNAME_LENGTH = 3
def validate_username(name):
if len(name) < MIN_USERNAME_LENGTH:
raise ValueError("too short")
if " " in name:
raise ValueError("no spaces allowed")
return name
for name in ["dana", "al", "big boss"]:
try:
print("valid:", validate_username(name))
except ValueError as error:
print(name, "->", error)valid: dana
al -> too short
big boss -> no spaces allowedУпражнение 3: Решает вызывающий код
Следующая функция выбрасывает ошибку для отрицательной цены:
def apply_discount(price, percent):
if price < 0:
raise ValueError("price cannot be negative")
return price - price * percent / 100Напишите две функции, которые используют её двумя разными способами:
report_price(price)— печатает цену после скидки 10% илиinvalid price, если была ошибка.price_or_zero(price)— возвращает цену после скидки 10% или 0, если была ошибка.
report_price(200)
report_price(-5)
print(price_or_zero(50))
print(price_or_zero(-5))Ожидаемый вывод:
180.0
invalid price
45.0
0Показать решение
def apply_discount(price, percent):
if price < 0:
raise ValueError("price cannot be negative")
return price - price * percent / 100
def report_price(price):
try:
print(apply_discount(price, 10))
except ValueError:
print("invalid price")
def price_or_zero(price):
try:
return apply_discount(price, 10)
except ValueError:
return 0
report_price(200)
report_price(-5)
print(price_or_zero(50))
print(price_or_zero(-5))180.0
invalid price
45.0
0Одна и та же функция, два разных способа обработки. Поэтому apply_discount выбрасывает ошибку, а не печатает сама: она не знает, что нужно вызывающему коду.
Упражнение 4: return None или raise
Две версии одной функции:
# A # B
def withdraw(balance, amount): def withdraw(balance, amount):
if amount > balance: if amount > balance:
print("Not enough money!") raise ValueError("not enough money")
return None return balance - amount
return balance - amountКто-то написал: balance = withdraw(100, 500). Что произойдёт в каждой версии и почему B лучше?
Показать решение
- A: печатается «Not enough money!», а
balanceполучаетNone. Программа продолжает работать с балансомNoneи упадёт позже где-то в другом месте — далеко от настоящей проблемы. - B:
ValueErrorвыбрасывается ровно в месте проблемы. Если вызывающий код его не обработал, Traceback указывает прямо на проблемное снятие. Если обработал — он сам решает, что показать. - Кроме того, B не печатает сама: возможно, вызывающий код — это сервер, которому нужно вернуть сообщение, а не печатать на экран.
Упражнение 5: Преобразование оценки
Напишите функцию parse_grade(text), которая преобразует текст в оценку. Она выбрасывает ValueError, если текст не число (это уже происходит в int), или если оценка вне диапазона от 0 до 100 — с сообщением, показанным в ожидаемом выводе (включая полученную оценку).
for text in ["87", "abc", "120"]:
try:
print("grade:", parse_grade(text))
except ValueError as error:
print("rejected:", error)Ожидаемый вывод:
grade: 87
rejected: invalid literal for int() with base 10: 'abc'
rejected: grade must be 0-100, got 120Показать решение
def parse_grade(text):
grade = int(text)
if grade < 0 or grade > 100:
raise ValueError("grade must be 0-100, got " + str(grade))
return grade
for text in ["87", "abc", "120"]:
try:
print("grade:", parse_grade(text))
except ValueError as error:
print("rejected:", error)grade: 87
rejected: invalid literal for int() with base 10: 'abc'
rejected: grade must be 0-100, got 120parse_grade не ловит ошибку из int — она позволяет ей пройти дальше к вызывающему коду, точно так же, как ошибке, которую выбрасывает сама.
Итоги
raise SomeError("message")выбрасывает ошибку. Функция немедленно останавливается, и значение не возвращается.- Функция обнаруживает и выбрасывает. Вызывающий код ловит и решает.
ValueErrorдля недопустимого значения,TypeErrorдля неверного типа. Не общийException.- Хорошее сообщение = каково правило + что получено.
- Одиночный
raiseвнутриexceptпередаёт ту же ошибку дальше. - Выбросить ошибку лучше, чем напечатать «ошибка» или вернуть специальное значение, потому что ошибку нельзя «забыть проверить».