К содержанию

Модуль 6 · Урок 6.2

Исключения и отладка

Перехват ожидаемой ошибки, raise, поиск причины по состоянию, assert и выбор проверочных примеров.

4 задачи

Функция 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)
Число: три↵ Введите целое число Число: 3↵ 6

Не окружай всю программу 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)
14 проверено 7 x — не число проверено x
Тонкости:зачем 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([]))
(20, 10) ValueError: Пустой список: наибольшего нет

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)
шаг 0 элемент 3 сумма 3 шаг 1 элемент -1 сумма 2 шаг 2 элемент 4 сумма 6

Такая печать показывает то же, что запуск по шагам в уроках: значения после каждой итерации. Ищи шаг, на котором значение впервые разошлось с ожидаемым, — ошибка в строках перед ним.

Другой способ — отладчик. В PyCharm и VS Code щелчок слева от номера строки ставит точку останова; запуск в режиме отладки (значок жука) останавливает программу на ней, показывает значения переменных и выполняет дальше по одной строке.

Перед сдачей решения на автоматическую проверку отладочную печать убирают: она станет лишним текстом в ответе.

assert

assert проверяет предположение автора программы. assert условие молчит, если условие истинно, и вызывает AssertionError, если ложно:

def double(number):    return number * 2 assert double(4) == 8assert double(-3) == -6assert double(5) == 11
AssertionError

Первые две проверки прошли молча, третья ложна — в ней ошибся автор проверки: 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Проверка автора; примеры подбирают на один элемент, пустой вход, отрицательные числа и повторы.

Проверьте себя

Ответь сначала сам, потом открой ответ.

Вопрос 1

Почему total_and_best мало проверить только на [1, 2, 3]? Какие примеры добавить?

ОтветСкрыть ответ
Ответ

Этот пример не проверит один элемент, пустой вход и отрицательные числа. Добавь [5], [-4, -2] и [] — для него ожидается ValueError.

Вопрос 2

В программе повторного ввода ввели 2.5, затем 4. Что она напечатает после этих вводов?

ОтветСкрыть ответ
Ответ

«Введите целое число» — int("2.5") даёт ValueError; затем 8.

Вопрос 3

Что напечатает цикл с finally из урока для списка ["x", "3"]?

ОтветСкрыть ответ
Ответ

«x — не число», «проверено x», 6 и «проверено 3».

Вопрос 4

Какие две строки отладочной печати выведет цикл из урока для списка [5, -5]?

ОтветСкрыть ответ
Ответ

«шаг 0 элемент 5 сумма 5» и «шаг 1 элемент -5 сумма 0».

Вопрос 5

Чем 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)

Запишите напечатанные строки через пробел в порядке вывода.

Что напечатает программа?

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)

Запишите напечатанную строку так, как её напечатала программа.

Напиши программу: сумма чисел среди ввода

Напиши программу: она читает три строки через input(), складывает те, что являются целыми числами, считает остальные и печатает сумму и количество остальных одним print. Запусти её, введи 5, x, 7 и запиши, что она напечатала.

Запишите два числа через пробел: сумму, затем количество нечисел.

Напиши функцию: деление без остановки

Напиши функцию safe_div(a, b): она возвращает a / b, а если b равно нулю, перехватывает ZeroDivisionError и возвращает None. Напечатай safe_div(7, 2) и safe_div(1, 0) одним print, запусти и запиши, что напечатала программа.

Запишите два значения через пробел, как их напечатала программа.