Модуль 6 · Урок 6.2
Исключения и отладка
Перехват ожидаемой ошибки, raise, поиск причины по состоянию, assert и выбор проверочных примеров.
Функция total_and_best из урока об аргументах считает сумму и наибольшее списка. Если файл с числами пуст, список тоже пуст, и вызов total_and_best([]) останавливает программу на numbers[0] с ошибкой IndexError: list index out of range.
Такое сообщение — исключение: сигнал, что операция не смогла выполниться обычным образом. Ожидаемое исключение программа может перехватить и обработать, о нарушенной договорённости — сообщить своим понятным исключением, а неожиданное должно остановить программу и показать ошибку.
Научимся перехватывать ошибку ввода, сделаем понятное сообщение для пустого списка и разберём, как искать ошибки и какими примерами проверять функцию.
try и except
try выполняет действие, которое может не получиться, except обрабатывает только ожидаемую проблему. Ошибки ValueError и ZeroDivisionError мы видели с первого урока. Теперь научимся их обрабатывать:
text = "abc"try: number = int(text)except ValueError: print("Нужно ввести целое число")else: print(number * 2)Блок try выполняется как обычно. Если в нём возникла ошибка ValueError, остаток блока пропускается и выполняется except ValueError. else выполняется, только если ошибки не было. Схема показывает, какие строки выполнились для двух значений:
"abc" строка с int начинает выполняться и прерывается ошибкой: number не получает значения, выполнение переходит к except, а else пропускается.Главное применение — повторный ввод: программа просит число, пока не получит целое. while True из урока о while повторяет попытку, а break в else завершает цикл после удачного ввода:
while True: text = input("Число: ") try: number = int(text) except ValueError: print("Введите целое число") else: breakprint(number * 2)Не окружай всю программу except: pass: так спрячутся и твои собственные ошибки. Перехватывай конкретное ожидаемое исключение и решай, что делать. Если вход по условию всегда правильный, ошибка чтения должна остановить программу — она укажет на неверно понятый формат.
После else можно добавить finally: его блок выполняется в любом случае — была ошибка или нет. Запусти цикл по шагам: для "7" после try выполняется else, для "x" — except, а finally — в обоих случаях.
for text in ["7", "x"]: try: number = int(text) except ValueError: print(text, "— не число") else: print(number * 2) finally: print("проверено", text)Тонкости:зачем finally
finally выполняется и при выходе из функции через return, поэтому в нём освобождают ресурсы. Для файла это уже делает with. Если процесс аварийно завершила операционная система, Python не гарантирует выполнение finally.
raise
raise сообщает о нарушении договорённости функции. У пустого списка нет наибольшего, и total_and_best должна сказать об этом понятно, а не IndexError из своей середины:
def total_and_best(numbers): if not numbers: raise ValueError("Пустой список: наибольшего нет") total = 0 best = numbers[0] for x in numbers: total += x if x > best: best = x return total, best print(total_and_best([5, -2, 10, 7]))print(total_and_best([]))not numbers истинно для пустого списка: пустые коллекции ложны. raise создаёт исключение с нашим текстом, и вызов прерывается. Вызывающий код перехватывает его, как любое другое:
try: total, best = total_and_best([])except ValueError: print("Файл пуст")else: print(total, best)Можно было вернуть None вместо наибольшего — это тоже договорённость. Нельзя молча вернуть 0: пустой список не имеет наибольшего 0. Какое поведение выбрано, пишут в описании функции.
Отладка
Ошибку ищут по состоянию: печатают промежуточные значения или идут отладчиком. Отладка — поиск причины, по которой программа работает неверно. Простой способ — временно печатать значения переменных на каждом шаге:
numbers = [3, -1, 4]total = 0for i, value in enumerate(numbers): total += value print("шаг", i, "элемент", value, "сумма", total)Такая печать показывает то же, что запуск по шагам в уроках: значения после каждой итерации. Ищи шаг, на котором значение впервые разошлось с ожидаемым, — ошибка в строках перед ним.
Другой способ — отладчик. В PyCharm и VS Code щелчок слева от номера строки ставит точку останова; запуск в режиме отладки (значок жука) останавливает программу на ней, показывает значения переменных и выполняет дальше по одной строке.
Перед сдачей решения на автоматическую проверку отладочную печать убирают: она станет лишним текстом в ответе.
assert
assert проверяет предположение автора программы. assert условие молчит, если условие истинно, и вызывает AssertionError, если ложно:
def double(number): return number * 2 assert double(4) == 8assert double(-3) == -6assert double(5) == 11Первые две проверки прошли молча, третья ложна — в ней ошибся автор проверки: 5 · 2 = 10. Так быстро проверяют свою функцию на нескольких примерах.
Тонкости:assert и python -O
При запуске python -O проверки assert отключаются. Поэтому проверять ввод пользователя через assert нельзя — для этого if и raise.
Проверочные примеры
Проверочные примеры выбирают так, чтобы каждый ловил свою ошибку. Один удачный пример ничего не доказывает. Проверь обычный случай, самый маленький вход, пустой вход, отрицательные числа и повторы, если они разрешены. Для total_and_best набор может быть таким:
[5, -2, 10, 7]— обычный случай, ловит неверную формулу;[5]— один элемент, ловит ошибку границы цикла;[]— пустой вход, ловитIndexErrorвместо выбранногоValueError;[-4, -2]— только отрицательные, ловят начальноеbest = 0;[3, 3]— повторы, ловят сравнение>=там, где это важно.
Для цикла от a до b добавь случай a == b, для пар соседей — списки длины 0, 1 и 2. Входы, которые функция по договорённости не принимает, проверять не нужно, но эту границу надо знать.
Итог
Исключение — сигнал о неудаче: ожидаемое перехватывают, о нарушенной договорённости сообщают сами, а остальное должно остановить программу. Вот что понадобится дальше.
try/exceptexcept Тип— только ожидаемая ошибка;else— если ошибки не было,finally— всегда.- Повторный ввод
while True+try,breakвelse;except: passне пишут. raiseraise ValueError("…")— понятное сообщение о нарушенной договорённости функции.- ОтладкаПечать состояния по шагам или точка останова в отладчике; отладочный вывод удаляют.
assertПроверка автора; примеры подбирают на один элемент, пустой вход, отрицательные числа и повторы.
Проверьте себя
Ответь сначала сам, потом открой ответ.
Почему total_and_best мало проверить только на [1, 2, 3]? Какие примеры добавить?
ОтветСкрыть ответ
Этот пример не проверит один элемент, пустой вход и отрицательные числа. Добавь [5], [-4, -2] и [] — для него ожидается ValueError.
В программе повторного ввода ввели 2.5, затем 4. Что она напечатает после этих вводов?
ОтветСкрыть ответ
«Введите целое число» — int("2.5") даёт ValueError; затем 8.
Что напечатает цикл с finally из урока для списка ["x", "3"]?
ОтветСкрыть ответ
«x — не число», «проверено x», 6 и «проверено 3».
Какие две строки отладочной печати выведет цикл из урока для списка [5, -5]?
ОтветСкрыть ответ
«шаг 0 элемент 5 сумма 5» и «шаг 1 элемент -5 сумма 0».
Чем raise ValueError(...) в начале total_and_best лучше, чем IndexError на numbers[0]?
ОтветСкрыть ответ
Сообщение называет причину — пустой список, а не индекс в середине функции; вызывающий код перехватывает именно ожидаемую ошибку.
Практика
Для проверки ответов нужен JavaScript. Условия, подсказки и решения доступны ниже.
Что напечатает программа?
for text in ["-5", " 8 "]: try: number = int(text) except ValueError: print("Нужно ввести целое число") else: print(number * 2)Запишите напечатанные строки через пробел в порядке вывода.
Подсказка
int понимает знак минус и пробелы по краям строки — ошибки не будет ни разу.
Разбор
int("-5") = −5, int(" 8 ") = 8: оба раза выполняется else и печатает удвоенное число.
Ответ: -10 16
Что напечатает программа?
def total_and_best(numbers): if not numbers: raise ValueError("Пустой список: наибольшего нет") total = 0 best = numbers[0] for x in numbers: total += x if x > best: best = x return total, best try: total, best = total_and_best([])except ValueError: total, best = 0, Noneprint(total, best)Запишите напечатанную строку так, как её напечатала программа.
Подсказка
Для пустого списка срабатывает raise, и выполняется блок except.
Разбор
Список пуст, not numbers истинно — функция вызывает ValueError. Вызов прерывается, except присваивает total = 0 и best = None.
Ответ: 0 None
Напиши программу: сумма чисел среди ввода
Напиши программу: она читает три строки через input(), складывает те, что являются целыми числами, считает остальные и печатает сумму и количество остальных одним print. Запусти её, введи 5, x, 7 и запиши, что она напечатала.
Запишите два числа через пробел: сумму, затем количество нечисел.
Подсказка
Цикл из трёх шагов, внутри — try с int(input()) и except ValueError.
Разбор
Например:
total = 0bad = 0for _ in range(3): try: total += int(input()) except ValueError: bad += 1print(total, bad)5 + 7 = 12, строка x — одна ошибка.
Ответ: 12 1
Напиши функцию: деление без остановки
Напиши функцию safe_div(a, b): она возвращает a / b, а если b равно нулю, перехватывает ZeroDivisionError и возвращает None. Напечатай safe_div(7, 2) и safe_div(1, 0) одним print, запусти и запиши, что напечатала программа.
Запишите два значения через пробел, как их напечатала программа.
Подсказка
return a / b внутри try, return None — в блоке except ZeroDivisionError.
Разбор
Например:
def safe_div(a, b): try: return a / b except ZeroDivisionError: return None print(safe_div(7, 2), safe_div(1, 0))7 / 2 = 3.5; деление на ноль перехвачено.
Ответ: 3.5 None