2.41. Отладка

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

2.41.1. Чтение трассировки стека

Когда скрипт вызывает исключение, а его никто не обрабатывает, REPL или IDE выводит трассировку стека – запись цепочки вызовов от самого внешнего скрипта вплоть до строки, где произошла ошибка.

Трассировка читается снизу вверх:

  • Нижняя строка указывает класс исключения и его сообщение (ValueError: invalid literal for int()...).

  • Каждый блок File "...", line N, in <name> выше неё – это кадр (frame), на один вызов глубже по мере движения вверх.

  • Самый верхний кадр – это место, где скрипт начался; самый нижний кадр – это место, где сработала ошибка.

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

2.41.3. Исследование объекта

Две встроенные функции отвечают на вопрос «что я могу с этим сделать»:

  • dir() – возвращает список всех имён, определённых для объекта: методы, атрибуты, дандеры, всё подряд.

  • help() – выводит строку документации (а на CPython – и сигнатуру) функции, метода или класса.

Используйте их вместе: dir находит имя, help объясняет, что оно делает.

2.41.3.1. Поиск имени с помощью dir

>>> dir([1, 2, 3])
['__add__', '__class__', '__contains__', '__delitem__',
 '__eq__', '__ge__', ..., 'append', 'clear', 'copy',
 'count', 'extend', 'index', 'insert', 'pop', 'remove',
 'reverse', 'sort']

Первая часть списка – это дандер-методы, унаследованные от каждого объекта; имена, которые стоит просмотреть, обычно идут после них. dir работает на чём угодно – на классе, экземпляре, модуле, встроенном типе:

>>> import json
>>> dir(json)
['__name__', 'dump', 'dumps', 'load', 'loads']

Вторая форма – это способ узнать, какие имена верхнего уровня модуль на самом деле предоставляет, не покидая REPL.

2.41.3.2. Поиск через help

Как только dir выявил кандидата, help его описывает:

>>> help(str.split)
split(sep=None, maxsplit=-1)
    Return a list of the words in the string, ...

На MicroPython help скромнее, чем на CPython – иногда это просто сигнатура, иногда однострочная строка документации, иногда ничего для встроенных C-функций. Это всё равно быстрая подсказка, когда всплывающая подсказка IDE недоступна.

2.41.4. Когда что-то зависает

Скрипт, который не возвращает управление, диагностировать сложнее, чем тот, что вызывает исключение. Частые причины:

  • Цикл while, условие которого никогда не становится ложным. Добавьте вывод переменной цикла на каждой итерации; если значение не меняется, значит, в теле цикла есть ошибка.

  • Блокирующий вызов, ожидающий ввода, который никогда не поступит – чтение из пустой очереди, бесконечный сон. Обрамите вызов выводами, чтобы увидеть, на какой строке скрипт застрял.

  • Бесконечная рекурсия. Трассировка, когда она в конце концов сработает (с RecursionError), обычно указывает прямо на неё.

Самый эффективный способ восстановления зависшего скрипта – кнопка stop в IDE, которая отправляет KeyboardInterrupt скрипту по USB. Прерывание проявляется как трассировка на текущей выполняемой строке – часто именно той строке, которая не возвращает управление.

Примечание

Если зависание сопротивляется всякой диагностике – скрипт выглядит правильным, трассировка прерывания указывает на встроенную функцию или код прошивки, а не на ваш скрипт, или тот же код работал на предыдущей сборке прошивки – причиной может быть ошибка в прошивке, а не в скрипте. Сократите скрипт до минимального воспроизводимого примера, который всё ещё зависает, и создайте сообщение на форуме OpenMV. Укажите версию прошивки, плату, на которой он запускался, и сокращённый скрипт.

2.41.5. Уберите диагностику перед выпуском

Стратегические выводы во время разработки – это прекрасно; но сотня вызовов print, оставленных в рабочем скрипте, засоряют вывод и используют кучу, которую могла бы использовать настоящая работа. Когда ошибка исправлена, уберите выводы (или защитите их флагом отладки, который можно отключить).

Для диагностики, которая должна оставаться в коде надолго, переключитесь с print() на модуль logging. Он присваивает уровень каждому сообщению (debug, info, warning, error) и позволяет одной настройкой заглушить тихие сообщения в продакшене:

import logging

log = logging.getLogger("main")
log.info("starting up")
log.debug("loaded config: %s", config)
log.warning("falling back to defaults")

Установка уровня логгера на logging.WARNING делает вызовы info и debug практически бесплатными (строка сообщения вообще не формируется), без необходимости закомментировать строки. Это делает logging правильным инструментом для постоянной диагностики; обычный print подходит для одноразовой.