2.41. Отладка¶
Большинство скриптов, которые не работают на камере, дают сбой одним из трёх способов: они вызывают исключение, выдают неверное значение или зависают. Для каждого из этих случаев есть свой набор инструментов.
2.41.1. Чтение трассировки стека¶
Когда скрипт вызывает исключение, а его никто не обрабатывает, REPL или IDE выводит трассировку стека – запись цепочки вызовов от самого внешнего скрипта вплоть до строки, где произошла ошибка.
Трассировка читается снизу вверх:
Нижняя строка указывает класс исключения и его сообщение (
ValueError: invalid literal for int()...).Каждый блок
File "...", line N, in <name>выше неё – это кадр (frame), на один вызов глубже по мере движения вверх.Самый верхний кадр – это место, где скрипт начался; самый нижний кадр – это место, где сработала ошибка.
Сначала прочитайте нижнюю строку, чтобы понять, что пошло не так, затем двигайтесь вверх, чтобы увидеть, как скрипт туда попал. Номера строк указывают на точные места в исходном коде скрипта.
2.41.2. Отладка с помощью print¶
Самый быстрый способ узнать, что делает скрипт, – вывести подозрительные значения. Три встроенные функции делают вывод более полезным:
repr()– возвращает строковое представление значения в стиле разработчика.print(repr(value))отличает"5"от5иNoneот"None", чего обычныйprint()сделать не может.type()– возвращает класс значения.print(type(value))– это способ узнать, не является ли переменная, которая «должна быть int», на самом деле строкой.len()– длина последовательности или коллекции. Удивительно большая доля ошибок – это проблемы с погрешностью на единицу или с несовпадением размеров.
print("got:", repr(value), "type:", type(value), "len:", len(value))
Вставьте print в каждую интересующую вас ветку – в обе ветви if, в каждый блок except, в тело цикла, который, как вы подозреваете, выполняется ноль раз. Цена – одна строка вывода; ценность – возможность узнать, действительно ли выполняется тот путь кода, который вы думаете, что выполняется.
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 подходит для одноразовой.