Исключения A: зачем нужна обработка ошибок, try и except
- Объяснить, что такое исключение (Exception) и что происходит с программой, если его не обработать
- Писать блок
try/except, который ловит ошибку и обрабатывает её - Обрабатывать несколько видов исключений по отдельности и получать детали ошибки с помощью
as - Понимать, почему «пустой»
except, который ловит всё, — опасная привычка
Что мы изучим?
- Что такое исключение (Exception).
- Почему настоящая программа должна обрабатывать ошибки, а не падать.
- Синтаксис
try/except. - Разную обработку для разных видов ошибок.
Зачем это нужно?
Представьте маленький сервер-калькулятор: один клиент отправляет "hello" вместо числа, и весь сервер падает. Все клиенты, которые придут после него, получают «Connection refused».
В реальном мире:
- Пользователь вводит «abc» в поле возраста. Приложение не должно закрываться.
- Файл, который программа пытается прочитать, удалён. Она должна сообщить об этом, а не упасть.
- Интернет пропал посреди запроса. Стоит попробовать снова или показать сообщение.
Ошибки будут происходить: плохой ввод, отсутствующие файлы, нестабильная сеть. Вопрос лишь в том, что программа делает, когда они происходят.
Объяснение с основ
Что такое исключение?
Исключение (Exception) — объект, который Python «выбрасывает» (Raise), когда возникает Runtime Error. Все ошибки, которые мы видели вчера (ValueError, ZeroDivisionError, TypeError…), — это исключения.
Когда выбрасывается исключение:
- Python немедленно останавливает текущую строку.
- Он ищет того, кто «поймает» (Catch) ошибку.
- Если никто не ловит, программа падает и печатается Traceback.
try / except — способ поймать.
Канатоходец (ваш код) может упасть (исключение). Без сетки падение — конец представления (падение программы). try = «представление идёт над сеткой». except = сетка, которая ловит падение, а потом вы решаете, что делать: попробовать снова, сообщить зрителям или перейти к следующему номеру.
Как это работает
try:— «попробуй выполнить этот блок».except SomeError:— «если внутриtryвыброшеноSomeError, перейди сюда и выполни этот блок вместо падения».- Если ошибки не было, блок
exceptпропускается. - После
try/exceptпрограмма продолжает как обычно.
Синтаксис
try:
# code that might fail
except ErrorType:
# what to do if ErrorType happened
# several error types, each handled differently
try:
...
except ValueError:
...
except ZeroDivisionError:
...
# get the error object itself (its message)
except ValueError as error:
print(error)
# same handling for several types
except (ValueError, TypeError):
...Первый пример кода
week-2/safe_age.py:
text = input("Enter your age: ")
try:
age = int(text)
print("Next year you will be", age + 1)
except ValueError:
print("That is not a whole number:", text)
print("Program continues normally.")Запуск 1 (корректный ввод):
Enter your age: 30
Next year you will be 31
Program continues normally.Запуск 2 (некорректный ввод):
Enter your age: abc
That is not a whole number: abc
Program continues normally.Разбор кода построчно
| Строка | Запуск 1 (30) | Запуск 2 (abc) |
|---|---|---|
text = input(...) | text = "30" | text = "abc" |
try: | Входит в блок | Входит в блок |
age = int(text) | Успешно: age = 30 | Выбрасывается ValueError, и Python сразу переходит к except |
print("Next year...") | Печатается | Пропускается. Мы до неё не доходим |
except ValueError: | Пропускается (ошибки нет) | Поймано! |
print("That is not...") | Не выполняется | Печатается |
print("Program continues...") | Печатается | Печатается, и программа не упала |
Важный момент: как только выброшена ошибка, остальные строки в try не выполняются. Поэтому внутрь try кладут только то, что зависит от успеха.
Ещё один пример
Надёжный калькулятор деления — с разной обработкой для каждого вида ошибки и циклом, который спрашивает снова. week-2/safe_divide.py:
def ask_number(prompt):
while True:
text = input(prompt)
try:
return float(text)
except ValueError:
print("Please type a number, for example 12 or 3.5")
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
print("Cannot divide by zero - returning 0")
return 0
x = ask_number("First number: ")
y = ask_number("Second number: ")
print("Result:", divide(x, y))Пример запуска:
First number: ten
Please type a number, for example 12 or 3.5
First number: 10
Second number: 0
Cannot divide by zero - returning 0
Result: 0Разбор:
return float(text)внутриtry— если преобразование удалось, функция немедленно возвращает значение и заодно выходит из цикла. Если не удалось,exceptпечатает сообщение, и цикл спрашивает снова. Так вы «вынуждаете» пользователя ввести корректное значение.- Каждая функция ловит ошибку, которая относится к ней:
ask_numberловитValueError(ввод),divideловитZeroDivisionError(вычисление). - Открытый вопрос: должна ли
divideвозвращать 0 при делении на ноль? Математически это неверный ответ! Во многих случаях лучше не ловить ошибку здесь и дать решить вызывающему коду. Поговорим об этом в уроке проraise.
as error — получение деталей ошибки
try:
number = int("12.5")
except ValueError as error:
print("Conversion failed:", error)Conversion failed: invalid literal for int() with base 10: '12.5'error — объект ошибки. Если его напечатать, увидите сообщение, которое появилось бы в последней строке Traceback.
Типичная ошибка
Ошибка 1: пустой except, который ловит всё.
try:
age = int(input("Age: "))
print("Double:", age * 2)
reslt = age / 0
except:
print("Something went wrong")Здесь три разные проблемы (ввод, деление на ноль и даже пропущенная буква в имени переменной), и все они спрятаны под «Something went wrong». Отладить это невозможно. И хуже всего: пустой except: ловит и KeyboardInterrupt, поэтому даже Ctrl + C не остановит программу, застрявшую в таком цикле!
Правило: ловите только конкретные ошибки, которые вы умеете обрабатывать. Все остальные идут дальше вверх и приводят к падению — и это хорошо, потому что так обнаруживаются баги.
Ошибка 2: огромный try.
try:
# 40 lines of code...
except ValueError:
print("Invalid value")Какая из 40 строк упала? Неизвестно. Кладите в try только строки, которые могут упасть с ловимой ошибкой, — как можно меньше.
Ошибка 3: молча «проглотить» ошибку.
try:
price = float(text)
except ValueError:
passpass означает «ничего не делать». Ошибка исчезает, а price может вообще не существовать, что вызовет непонятный NameError несколькими строками ниже. Всегда делайте что-нибудь: сообщение, объяснённое значение по умолчанию или запись в лог.
Практика
Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.
Упражнение 1: Безопасное преобразование в число
Напишите функцию to_number(text), которая возвращает текст как целое число, а если текст не число — возвращает None, и программа при этом не падает.
print(to_number("42"))
print(to_number("hello"))
print(to_number("7"))Ожидаемый вывод:
42
None
7Показать решение
def to_number(text):
try:
return int(text)
except ValueError:
return None
print(to_number("42"))
print(to_number("hello"))
print(to_number("7"))42
None
7int("hello") выбрасывает ValueError. Без try программа упала бы. С except ValueError мы решаем, что произойдёт вместо этого.
Упражнение 2: Разная обработка для каждого вида ошибки
Напишите функцию safe_divide(a_text, b_text), которая получает два текста, преобразует их в числа и делит. Она возвращает:
- результат деления, если всё корректно;
"numbers only", если один из текстов не число;"cannot divide by zero", если делитель равен 0.
print(safe_divide("10", "4"))
print(safe_divide("10", "0"))
print(safe_divide("ten", "2"))Ожидаемый вывод:
2.5
cannot divide by zero
numbers onlyПоказать решение
def safe_divide(a_text, b_text):
try:
return int(a_text) / int(b_text)
except ValueError:
return "numbers only"
except ZeroDivisionError:
return "cannot divide by zero"
print(safe_divide("10", "4"))
print(safe_divide("10", "0"))
print(safe_divide("ten", "2"))2.5
cannot divide by zero
numbers onlyУ каждого вида ошибки свой except с подходящей обработкой. Python проверяет блоки except по порядку и входит в первый подходящий.
Упражнение 3: Показываем сообщение об ошибке
Следующий код падает с ValueError. Оберните его в try, поймайте ошибку через except ValueError as error и напечатайте текст Could not convert:, а за ним сообщение, которое Python сохранил в error:
number = int("12abc")Ожидаемый вывод:
Could not convert: invalid literal for int() with base 10: '12abc'Показать решение
try:
number = int("12abc")
except ValueError as error:
print("Could not convert:", error)Could not convert: invalid literal for int() with base 10: '12abc'as error сохраняет объект ошибки в переменной. Когда вы его печатаете, получаете сообщение, которое создал Python.
Упражнение 4: Выбор из списка
У вас есть список имён. Напишите функцию pick(names, position_text), которая получает позицию в виде текста (начиная с 1) и возвращает имя на этой позиции. Она возвращает "not a number" для ввода, который не является числом, и "no such position" для несуществующей позиции.
names = ["Dana", "Avi", "Noa"]
print(pick(names, "2"))
print(pick(names, "abc"))
print(pick(names, "9"))Ожидаемый вывод:
Avi
not a number
no such positionПоказать решение
def pick(names, position_text):
try:
position = int(position_text)
return names[position - 1]
except ValueError:
return "not a number"
except IndexError:
return "no such position"
names = ["Dana", "Avi", "Noa"]
print(pick(names, "2"))
print(pick(names, "abc"))
print(pick(names, "9"))Avi
not a number
no such positionДве разные ошибки в двух местах: int(...) может выбросить ValueError, а обращение к списку — IndexError.
Упражнение 5: except, который ловит всё
def read_age(text):
try:
return int(text)
except:
return 0- Почему
except:без типа ошибки опасен? - Приведите пример настоящего бага, который скрыл бы этот код.
- Исправьте код.
Показать решение
- Пустой
except:ловит любую ошибку — включая те, о которых мы не подумали, — и скрывает их. Код продолжает работать, будто всё в порядке, с неверным значением. - Если кто-то по ошибке передаст
Noneвместо текста, будет выброшенTypeError— настоящий баг в коде, который вызвал функцию. Вместо того чтобы увидеть его, мы молча получим возраст 0. (ДажеCtrl + Cловится пустымexcept:.) - Ловить только ту ошибку, которую мы ожидаем:
def read_age(text):
try:
return int(text)
except ValueError:
return 0
print(read_age("30"))
print(read_age("thirty"))30
0Итоги
- Исключение (Exception) = ошибка, которую Python выбрасывает во время выполнения. Если её никто не ловит, программа падает.
try= «попробуй».except SomeError= «если произошла эта ошибка, сделай это вместо падения».- Как только выброшена ошибка, остаток
tryпропускается. except SomeError as errorдаёт доступ к сообщению об ошибке.- Ловите только конкретные ошибки, с маленьким
try. Никаких пустыхexcept:и никакихpass, которые проглатывают ошибки.