Отладка программ

Цель: научиться исправлять синтаксические и семантические ошибки в программах.

Пример

Синтаксические ошибки

Условие задачи звучит следующим образом:

Выведите на экран значения функции \(y=\sqrt{x-5}\). Начальное и конечное значение x, а также шаг его изменения вводится с клавиатуры. Вычисление y вынесите в отдельную функцию. Если значение x не определено для функции, то \(y=0\).

Решение задачи выглядит следующим образом:

# импортируем функцию для вычисления корня
from math import sqrt

# определение функции
def func(x):
    return sqrt(x - 5)

# запрашиваем ввод необходимых данных
xn = float(input("Введите начальное значение x "))
xk = float(input("Введите конечное значение x ")
step = float(input("Введите шаг изменения x "))

print("Значения функции:)

x = nx # начальное значение x
while x < xk
    y = func(x) # вычисляем y
    print(f"{x:.2f} {y:.2f}")
    x = x + 1

Скачайте файл с решением ex01.py и откройте его в среде разработки.

Как сохранить файл?

Чтобы сохранить этот файл, щёлкните по ссылке правой кнопкой мыши, и выберите пункт меню Сохранить объект как….

Запустите программу. Python сообщит о наличии ошибки:

  File "ex01.py", line 11
    step = float(input("Введите шаг изменения x "))
    ^
SyntaxError: invalid syntax

Зачастую, стоит искать синтаксическую ошибку выше той строки, что указана в сообщении об ошибке.

В 10 строке не хватает закрывающей скобки. Добавьте её и запустите программу повторно.

Следующая ошибка:

  File "ex01.py", line 13
    print("Значения функции:)
                             ^
SyntaxError: EOL while scanning string literal

EOL while scanning string literal означает, что, скорее всего, проблема в нехватке кавычек в написании строки. Добавьте недостающую кавычку в 13 строке. Запустите программу ещё раз.

File "ex01.py", line 16
    while x < xk
               ^
SyntaxError: invalid syntax

Очередная ошибка. Вспоминаем, что блочные инструкции должны заканчиваться символов двоеточия :, показывая, где начинается вложенный набор команд. Добавьте недостающее двоеточие, и запустите программу.

Синтаксических ошибок больше нет, и программа предлагает ввести необходимые данные.

Ошибки времени выполнения

Введите для тестирования следующие данные:

Введите начальное значение x 2
Введите конечное значение x 10
Введите шаг изменения x 1

Возникнет runtime-ошибка (ошибка времени выполнения):

Traceback (most recent call last):
  File "ex01.py", line 15, in <module>
    x = nx # начальное значение x
NameError: name 'nx' is not defined

Ошибка NameError означает, что используется переменная, которой не присвоено значение. Также всё может объясняться опечаткой в имени переменной. В 15 строке исправьте имя nx на xn. Запустите программу и повторите ввод данных.

Traceback (most recent call last):
  File "ex01.py", line 17, in <module>
    y = func(x) # вычисляем y
  File "ex01.py", line 6, in func
    return sqrt(x - 5)
ValueError: math domain error

Следующее сообщение длиннее предыдущих, так как проблема связана с вычислениями происходящими в функции. Программа (модуль) вызвала функцию func в 17 строке. Интерпретатор начал выполнение функции, и произошла ошибка (строка 6). Поэтому сообщение об ошибке содержит ссылку на две строки - где в функции произошла исключительная ситуация, а также где в программе была вызвана функция, выполнение которой привело к исключительной ситуации. Достаточно исправить ошибку в функции (6 строка). Поэтому сообщения об ошибке следует читать снизу вверх.

Есть два варианта исправить данную ошибку - использовать обработку исключений (try...except) или условный оператор (if). Оба варианта приведены ниже:

# исправление с помощью if
def func(x):
    # если x меньше 5, возвращаем 0
    if x < 5:
        return 0
    return sqrt(x - 5)
# исправление с помощью try..except
def func(x):
    try:
        return sqrt(x - 5)
    except ValueError:
        # если возникает ошибка, вернём 0
        return 0

В данном случае, первый вариант лучше. Но в некоторых ситуациях, выгоднее сначала попытаться выполнить команду, и, в случае возникновения ошибки, использовать запасной план. Такой подход в программировании характеризует следующий слоган: Проще просить прощения, чем просить разрешения (применяйте такой подход только в программировании, а не в реальной жизни 🎓).

Семантические ошибки

Запустите программу со следующими входными данными:

Введите начальное значение x 2
Введите конечное значение x 10
Введите шаг изменения x 2

Результат работы программы будет выглядеть следующим образом:

Значения функции:
2.00 0.00
3.00 0.00
4.00 0.00
5.00 0.00
6.00 1.00
7.00 1.41
8.00 1.73
9.00 2.00

С клавиатуры был введён шаг равный двум, при этом значение x внутри цикла всегда меняется на единицу. Очевидно, что ошибка связана с изменением переменной цикла:

x = x + 1

Несмотря на простоту данной смысловой ошибки, в качестве тренировки, воспользуемся модулем logging. В простых программах для вывода промежуточных результатов вычислений, можно воспользоваться функцией print.

В начале программы добавьте импорт модуля logging и его настройку:

import logging
logging.basicConfig(level=logging.DEBUG)

Добавим вывод сообщений перед циклом, внутри цикла (вывод переменной цикла), и после цикла:

logging.debug("Цикл начинается") # добавьте эту строку
while x < xk:
    y = func(x) # вычисляем y
    print(f"{x:.2f} {y:.2f}")
    x = x + 1
    logging.debug(f"x={str(x)}") # добавьте эту строку
logging.debug("Цикл завершился") # добавьте эту строку

Запустите программу с произвольными данными, и шагом отличающимся от 1. Сообщения модуля logging могут выглядеть следующим образом:

DEBUG:root:Цикл начинается
DEBUG:root:x=3.0
DEBUG:root:x=4.0
DEBUG:root:x=5.0
DEBUG:root:x=6.0
DEBUG:root:x=7.0
DEBUG:root:x=8.0
DEBUG:root:x=9.0
DEBUG:root:x=10.0
DEBUG:root:Цикл завершился

Шаг изменения цикла всегда меняется на 1, несмотря на то, что для хранения шага используется переменная step. Исправьте изменение шага следующим образом:

x = x + step

Теперь шаг будет учитываться в работе цикла.

После исправления ошибки журналирование можно отключить. Для этого добавьте в начале программы следующую строку:

import logging
logging.basicConfig(level=logging.DEBUG)
logging.disable() # добавьте эту строку
Уведомление

Журналирование полезно в крупных программах, особенно если они остаются запущенными длительное время, а логи их работы записываются в файлы.

Теперь программа работает почти правильно, но осталась ещё одна семантическая ошибка. Найдите и исправьте её.

Задания для самостоятельной работы

Скачайте архив, доступный по данной ссылке.

Как сохранить файл?

Чтобы сохранить этот файл, щёлкните по ссылке правой кнопкой мыши, и выберите пункт меню Сохранить объект как….

Распакуйте архив, и откройте папку debug. Не удаляйте архив после распаковки. В данной папке располагаются 11 файлов с решениями задач, которые содержат ошибки различного типа. Условия задач указаны в начале каждого файла.

Последовательно открывайте файлы и, применяя различные практики отладки, исправьте ошибки в исходном коде программ.

Если вы считаете, что лучше начать отладку какой-либо программы сначала, возьмите исходную версия задания из архива.

Рекомендации по отладке программ

Список ошибок

Имя ошибки Пояснение
SyntaxError Ошибка в синтаксисе: пропущен необходимый символ, не хватает открывающей или закрывающей скобки и т.д.
NameError В выражении используется переменная, которой ещё не было присвоено значение. Возможна опечатка в имени переменной, функции, класса.
ValueError Функция ожидала аргумент другого типа. Например в функцию int передана строка, которая не содержит целое число.
TypeError В выражении используются несовместимые типы. Например попытка сложить строку и число. Или в функцию передано неправильное количество аргументов. Для проверки типа переменной можно использовать функцию type().
KeyError Попытка получить значение по ключу, которого нет в словаре.
IndexError Попытка получить элемент из списка, или символ из строки по индексу, которого нет в последовательности.

Исправление синтаксических ошибок

  • Убедитесь, что для имён переменных не используются служебные слова языка.
  • В начале нового блока расположено двоеточие (после for, while, if, def, class).
  • Соблюдены правила использования отступов.
  • Строки обладают парными кавычками.
  • Для каждой открывающей скобки есть своя закрывающая – (), {}, [].

Исправление ошибок времени выполнения

Программа ничего не выводит на экран

Убедитесь, что описанные в программе функции или методы классов вызываются, для получения результата и вывода их на экран с помощью print.

Программа выполняется без остановки

Это может произойти за-за бесконечного цикла. Чтобы найти цикл, который не завершается, разместите перед каждым циклом в программе print c сообщением “Начало цикла N”, а после цикла - “Конец цикла N”, где N - номер цикла по-порядку. Таким образом можно определить, какой из циклов в программе выполняется бесконечно:

print("Начало цикла 1")
x = 1
while x < 10:
    y = x * x
    print(x)
print("Конец цикла 1")

Когда бесконечный цикл найден, разместите в конце цикла print с выводом переменной цикла и условия завершения цикла (для while):

x = 1
while x < 10:
    y = x * x
    print(x, x < 10)

В примере выше получим следующее:

...
1 True
1 True
1 True
...

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

x = 1
while x < 10:
    y = x * x
    x = x + 1 # изменение переменной цикла

В программе слишком много функций print для отладки

Для отладочных сообщений в больших программах удобнее использовать модуль журналирования logging:

import logging
logging.basicConfig(level=logging.DEBUG)
# разкоментировать, чтобы отключить отладочные сообщения
# logging.disable()

logging.debug("Начало цикла")
x = 1
while x < 10:
    y = x * x
    logging.debug(x, x < 10)
logging.debug("Конец цикла")

Используя данный модуль, можно отключить все отладочные сообщения вызовом одной функции logging.disable().

Выражение слишком большое, чтобы понять где ошибка

Длинное выражение можно разбить на несколько меньших выражений, чтобы точнее определить источник ошибки.

goods = {
    "cars": {"BMW": "1000", "Audi": "900", "Honda": "300"},
    "bikes": []
}

# сложная для чтения строка
audi_price = int(goods["cars"].get("Audi", "0"))

При возникновении ошибки в последней строке, сложно определить конкретную операцию, которая её вызвала. В таком случае сложною строку лучше разбить на ряд простых действий:

cars_dict = goods["cars"] # словарь с машинами
price_str = cars_dict.get("Audi", "0") # Цена машины (строка)
audi_price = int(price_str) # преобразование строки в целое число

Функция или метод класса возвращают неправильное значение

Если функция или метод содержат return со сложным выражением, то его не получится вывести на экран до возврата значения. Например вместо:

return self.name.upper()[0] + self.lastname.upper()

можно записать:

full_name = self.name.upper()[0] + self.lastname.upper()
print(full_name)
return full_name

Таким образом, можно вывести возвращаемое значение на экран, до завершения функции.