Отладка A: что такое баг, виды ошибок и чтение Stack Trace

Цели урока
  • Определить, что такое баг (Bug), и различать Syntax Error, Runtime Error и Logical Error
  • Читать Stack Trace — определять тип ошибки, сообщение, файл и строку
  • Знать распространённые ошибки в Python и причину каждой из них
  • Понимать, почему Logical Error найти труднее всего

Что мы изучим?

  • Что такое баг (Bug).
  • Три вида ошибок: Syntax Error, Runtime Error, Logical Error.
  • Как читать Stack Trace (Traceback) — сообщение об ошибке в Python.
  • Самые распространённые ошибки и что говорит каждая из них.

Зачем это нужно?

Разработчики тратят огромную часть времени на поиск и исправление багов. Разница между новичком и профессионалом не в том, что у профессионала нет багов, а в том, что он читает сообщение об ошибке и за секунды понимает, куда смотреть. Новички видят много красного текста и паникуют. К концу урока красный текст станет картой.

Объяснение с основ

Что такое баг?

Баг (Bug) — разрыв между тем, что программа должна делать, и тем, что она делает на самом деле. Иногда программа падает, а иногда «работает», но возвращает неверный результат.

Почему это называется Bug?

В 1947 году инженеры компьютера Mark II нашли мотылька (bug — «жук», «насекомое»), застрявшего в одном из реле, и вклеили его в рабочий журнал с подписью «first actual case of bug being found». Слово использовалось для технических неисправностей и раньше, но именно эта история сделала его знаменитым.

Три вида ошибок

ВидКогда обнаруживаетсяСообщает ли о ней Python?Пример
Syntax ErrorДо запуска (при проверке синтаксиса)Да, и не выполняется ни одна строкаПропущенный : или скобка
Runtime Error (исключение, Exception)Во время выполнения, на проблемной строкеДа, а строки перед ней уже выполнилисьДеление на 0, несуществующая переменная
Logical ErrorНикогда автоматическиНет! Код выполняется «успешно»+ вместо -, перевёрнутое условие

Помните тему о компьютере, операционной системе и сети? Syntax Error обнаруживается, когда Python переводит код в байт-код (Bytecode), до выполнения. Runtime Error возникает, когда PVM выполняет определённую строку и терпит неудачу.

Logical Error — самая опасная: ни сообщения, ни красного цвета, просто неверный результат. Её находят, только если проверяют, что результат правильный.

Stack Trace — карта ошибки

Когда возникает Runtime Error, Python печатает Traceback (его также называют Stack Trace): путь, который прошёл код до ошибки.

Traceback (most recent call last):
  File "shop.py", line 9, in <module>
    total = checkout(cart)
  File "shop.py", line 5, in checkout
    return subtotal / count
ZeroDivisionError: division by zero

Читайте снизу вверх:

  1. Последняя строка — тип ошибки (ZeroDivisionError) и сообщение (division by zero). Всегда начинайте отсюда.
  2. Строка над ней — где именно: файл shop.py, строка 5, внутри функции checkout, и сам код.
  3. Ещё выше — кто вызвал эту функцию: строка 9, на верхнем уровне файла (<module>).

Почему «Stack»? Потому что каждый вызов функции кладётся в стек вызовов (Call Stack). Traceback показывает стопку в момент падения — от первого вызова (сверху) до последнего (снизу).

Распространённые ошибки в Python

ОшибкаЗначениеЧастая причина
SyntaxErrorКод — недопустимый синтаксисПропущенный :, скобки, кавычки
IndentationErrorНеправильный отступСмешение пробелов, непоследовательный отступ
NameErrorИмя не определеноОпечатка, использование до определения
TypeErrorОперация над неподходящим типом"5" + 3, вызов функции с неверным числом аргументов
ValueErrorПравильный тип, недопустимое значениеint("hello")
ZeroDivisionErrorДеление на нольЗнаменатель, который не проверили
IndexErrorИндекс за пределами спискаitems[5], когда элементов 3
KeyErrorКлюч, которого нет в словаре(изучим в теме структур данных и функций)
AttributeErrorУ объекта нет такого атрибута/метода"abc".push(), опечатка в имени метода
FileNotFoundErrorФайл не существуетНеверный путь, рабочая папка (Working Directory)

Синтаксис

Нового синтаксиса нет. Инструмент — чтение. Шаблон любого Traceback:

Traceback (most recent call last):
  File "<file>", line <N>, in <function or <module>>
    <the code on that line>
  ...                                  ← more frames, each a function call
<ErrorType>: <message>                 ← START READING HERE

И для Syntax Error (без Traceback, потому что ничего не выполнялось):

  File "<file>", line <N>
    <the code>
        ^                              ← where Python noticed the problem
SyntaxError: <message>

Первый пример кода

week-2/bug_report.py:

def average(numbers):
    total = sum(numbers)
    return total / len(numbers)

scores = []
print("Average score:", average(scores))

Вывод:

Traceback (most recent call last):
  File "C:\dev\kodkod6\week-2\bug_report.py", line 7, in <module>
    print("Average score:", average(scores))
                            ^^^^^^^^^^^^^^^
  File "C:\dev\kodkod6\week-2\bug_report.py", line 3, in average
    return total / len(numbers)
           ~~~~~~^~~~~~~~~~~~~~
ZeroDivisionError: division by zero

Разбор кода построчно

Читаем Traceback снизу вверх:

Строка выводаЧто она нам говорит
ZeroDivisionError: division by zeroЧто произошло: деление на ноль.
line 3, in average + return total / len(numbers)Где: строка 3, внутри average. ~~~^~~~ точно отмечает операцию деления.
line 7, in <module> + average(scores)Кто вызвал: строка 7, на верхнем уровне. <module> = «не внутри функции».

Делаем вывод: len(numbers) равно 0, то есть список пуст. А кто передал пустой список? Строка 7 передала scores, который равен []. Настоящий баг в том, что мы не обработали случай пустого списка. Строка, которая упала (3), — это симптом, а решение, что делать с пустым списком, — исправление:

def average(numbers):
    if len(numbers) == 0:
        return 0
    return sum(numbers) / len(numbers)

Ещё один пример

Три вида ошибок в одной и той же функции. Функция должна вычислять цену после процентной скидки.

Syntax Error:

def price_after_discount(price, percent)
    return price - price * percent / 100
  File "discount.py", line 1
    def price_after_discount(price, percent)
                                            ^
SyntaxError: expected ':'

Не хватает двоеточия. Python не выполнил ни одной строки.

Runtime Error:

def price_after_discount(price, percent):
    return price - price * percent / 100

print(price_after_discount("100", 10))
Traceback (most recent call last):
  File "discount.py", line 4, in <module>
    print(price_after_discount("100", 10))
  File "discount.py", line 2, in price_after_discount
    return price - price * percent / 100
                   ~~~~~~~~~~~~~~~~^~~~~
TypeError: unsupported operand type(s) for /: 'str' and 'int'

Синтаксис допустим, но во время выполнения price — текст. "100" * 10 «работает» (повторяет текст 10 раз!), но затем деление на 100 падает. ^ указывает на /.

Logical Error:

def price_after_discount(price, percent):
    return price - percent / 100

print(price_after_discount(100, 10))
99.9

Никакой ошибки! Но скидка 10% на 100 должна вернуть 90.0. Мы забыли умножить на price. Python не может знать, что мы имели в виду. Только проверка по ожидаемому результату это выявит.

Типичная ошибка

Ошибка 1: читать Traceback сверху и паниковать. Всегда начинайте с последней строки: тип и сообщение. Затем строка над ней. Чаще всего этого достаточно.

Ошибка 2: исправлять строку, которая упала, а не причину. В первом примере «исправление» вроде return total / (len(numbers) + 1) убирает ошибку, но возвращает неверный результат во всех случаях! Это превратило Runtime Error в Logical Error. Ещё хуже.

Ошибка 3: искать Syntax Error только в строке, указанной в сообщении. Иногда проблема в предыдущей строке:

prices = [10, 20, 30
total = sum(prices)
  File "test.py", line 1
    prices = [10, 20, 30
             ^
SyntaxError: '[' was never closed

Python дошёл до строки 2 и обнаружил, что скобка из строки 1 так и не закрыта. В новых версиях сообщение точное, но в общем случае: если отмеченная строка выглядит нормально, проверьте строку перед ней.

Практика

Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.

Упражнение 1: Какой вид ошибки?

Для каждого фрагмента — это SyntaxError, Runtime Error или Logical Error? И в чём проблема?

# A
if age > 18
    print("adult")

# B
print(total_price)        # total_price was never defined

# C
def area(width, height):
    return width + height   # should be the area of a rectangle
Показать решение
  • A — SyntaxError: в конце строки if не хватает двоеточия :. Не выполняется ни одна строка.
  • B — Runtime Error (NameError): синтаксис допустим, но во время выполнения Python не может найти переменную с именем total_price.
  • C — Logical Error: ошибки нет вообще, но результат неверный — площадь вычисляется умножением, width * height. Такую ошибку находят, только если проверяют результат.

Упражнение 2: Читаем Stack Trace

Traceback (most recent call last):
  File "shop.py", line 12, in <module>
    print(checkout([20, 30], 0))
  File "shop.py", line 7, in checkout
    return average_price(prices, count)
  File "shop.py", line 3, in average_price
    return sum(prices) / count
ZeroDivisionError: division by zero
  1. Какой тип ошибки и какое сообщение?
  2. В каком файле, на какой строке и в какой функции она произошла?
  3. Откуда пришло проблемное значение?
Показать решение
  1. ZeroDivisionError: division by zero — деление на ноль. Сначала читайте последнюю строку.
  2. В файле shop.py, строка 3, в функции average_price — это самая нижняя строка в списке.
  3. Читаем вверх: average_price вызвана из строки 7 в checkout, а checkout вызвана из строки 12 с count = 0. Значит, проблема началась в вызове на строке 12, который передал 0.

Упражнение 3: Логический баг в переводе температуры

def to_celsius(fahrenheit):
    return fahrenheit - 32 * 5 / 9

print(to_celsius(212))
print(to_celsius(32))

Результаты должны быть 100 и 0, но получается что-то другое. Найдите баг и исправьте его.

Ожидаемый вывод:

100.0
0.0
Показать решение
def to_celsius(fahrenheit):
    return (fahrenheit - 32) * 5 / 9


print(to_celsius(212))
print(to_celsius(32))
100.0
0.0

Порядок операций: умножение и деление выполняются раньше вычитания, поэтому исходный код вычислял fahrenheit - (32 * 5 / 9). Скобки заставляют вычитание выполниться первым. Это Logical Error — никакого сообщения об ошибке, просто неверный результат.

Упражнение 4: TypeError

age = 25
print("Age: " + age)
  1. Какую ошибку вы получите и почему?
  2. Исправьте её двумя разными способами.

Ожидаемый вывод:

Age: 25
Показать решение
  1. TypeError: can only concatenate str (not "int") to str — нельзя соединять текст и число через +.
  2. Два способа:
age = 25
print("Age: " + str(age))
Age: 25
age = 25
print("Age:", age)
Age: 25

Первый переводит число в текст. Второй передаёт print два значения, и он добавляет между ними пробел.

Упражнение 5: Среднее пустого списка

def average(numbers):
    return sum(numbers) / len(numbers)

print(average([80, 90]))
print(average([]))
  1. Что произойдёт при втором вызове и какую ошибку вы получите?
  2. Исправьте функцию так, чтобы для пустого списка она возвращала 0.

Ожидаемый вывод:

85.0
0
Показать решение
  1. len([]) равно 0, поэтому вы получите ZeroDivisionError: division by zero — ошибку времени выполнения.
  2. Проверьте этот случай до деления:
def average(numbers):
    if len(numbers) == 0:
        return 0
    return sum(numbers) / len(numbers)


print(average([80, 90]))
print(average([]))
85.0
0

Итоги

  • Баг = разрыв между желаемым и фактическим поведением.
  • Syntax Error — до запуска, не выполняется ни одна строка. Runtime Error — посреди выполнения, с Traceback. Logical Error — без сообщения, просто неверный результат.
  • Читайте Traceback снизу вверх: тип + сообщение → строка и функция → кто вызвал.
  • Строка, которая упала, иногда лишь симптом. Ищите причину.
  • Logical Error обнаруживается, только когда вы сравниваете результат с ожидаемым значением.

Официальная документация