2.41. Debugowanie

Większość skryptów, które zawodzą na kamerze, zawodzi na jeden z trzech sposobów: zgłaszają wyjątek, zwracają błędną wartość albo się zawieszają. Każdy z tych przypadków ma inny zestaw narzędzi.

2.41.1. Czytanie śladu wywołań (traceback)

Gdy skrypt zgłosi wyjątek, a nic go nie obsłuży, REPL lub IDE wypisuje ślad wywołań (traceback) – zapis łańcucha wywołań od najbardziej zewnętrznego skryptu aż do wiersza, który zgłosił błąd.

Ślad wywołań czyta się od dołu do góry:

  • Dolny wiersz podaje klasę wyjątku oraz jego komunikat (ValueError: invalid literal for int()...).

  • Każdy blok File "...", line N, in <name> powyżej to ramka (frame) – każda kolejna w górę jest o jedno wywołanie głębiej.

  • Najwyższa ramka to miejsce, w którym skrypt się rozpoczął; najniższa ramka to miejsce, w którym wystąpił błąd.

Najpierw przeczytaj dół, aby dowiedzieć się, co poszło nie tak, a następnie idź w górę, aby zobaczyć, jak skrypt do tego doszedł. Numery wierszy wskazują dokładne miejsca w kodzie źródłowym skryptu.

2.41.3. Badanie obiektu

Dwie wbudowane funkcje odpowiadają na pytanie „co mogę z tym zrobić”:

  • dir() – zwraca listę wszystkich nazw zdefiniowanych na obiekcie: metod, atrybutów, dunderów, wszystkiego.

  • help() – wypisuje docstring (a w CPythonie również sygnaturę) funkcji, metody lub klasy.

Używaj ich razem: dir znajduje nazwę, a help wyjaśnia, co ona robi.

2.41.3.1. Znajdowanie nazwy za pomocą dir

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

Pierwsza część listy to metody dunder, dziedziczone z każdego obiektu; nazwy warte uwagi znajdują się zwykle po nich. dir działa na wszystkim – na klasie, instancji, module, typie wbudowanym:

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

Ta druga forma pozwala dowiedzieć się, jakie nazwy najwyższego poziomu moduł faktycznie udostępnia, bez wychodzenia z REPL.

2.41.3.2. Sprawdzanie za pomocą help

Gdy dir wskaże kandydata, help go opisuje:

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

W MicroPythonie help jest uboższe niż w CPythonie – czasem to tylko sygnatura, czasem jednowierszowy docstring, a czasem nic w przypadku wbudowanych funkcji w C. Mimo to jest szybkim przypomnieniem, gdy podpowiedź IDE nie jest pod ręką.

2.41.4. Gdy coś się zawiesza

Skrypt, który nie zwraca sterowania, jest trudniejszy do zdiagnozowania niż taki, który zgłasza wyjątek. Najczęstsze przyczyny:

  • Pętla while, której warunek nigdy nie staje się fałszywy. Dodaj wypisywanie zmiennej pętli w każdej iteracji; jeśli wartość się nie zmienia, ciało pętli ma błąd.

  • Wywołanie blokujące oczekujące na dane wejściowe, które nigdy nie nadchodzą – odczyt z pustej kolejki, uśpienie bez końca. Otocz to wywołanie wypisywaniem, aby zobaczyć, na którym wierszu skrypt utknął.

  • Nieskończona rekurencja. Ślad wywołań, gdy ostatecznie się ujawni (z RecursionError), zazwyczaj wskazuje wprost na nią.

Najskuteczniejszym sposobem odzyskania kontroli nad zawieszonym skryptem jest przycisk stop w IDE, który wysyła do skryptu KeyboardInterrupt przez USB. Przerwanie ujawnia się jako ślad wywołań w aktualnie wykonywanym wierszu – często dokładnie w tym wierszu, który nie zwraca sterowania.

Informacja

Jeśli zawieszenie opiera się każdej diagnostyce – skrypt wydaje się poprawny, ślad wywołań po przerwaniu wskazuje na funkcję wbudowaną lub na kod oprogramowania układowego zamiast na Twój skrypt, albo ten sam kod działał na poprzedniej wersji oprogramowania układowego – przyczyną może być błąd oprogramowania układowego, a nie błąd skryptu. Zredukuj skrypt do najmniejszego przykładu, który nadal się zawiesza, i zgłoś problem na forum OpenMV. Dołącz wersję oprogramowania układowego, płytkę, na której był uruchamiany, oraz uproszczony skrypt.

2.41.5. Usuń diagnostykę przed wdrożeniem

Strategicznie rozmieszczone wywołania print podczas tworzenia kodu są świetne; sto wywołań print pozostawionych w skrypcie produkcyjnym zaśmieca wyjście i zużywa stertę, którą mogłaby wykorzystać właściwa praca. Po naprawieniu błędu usuń wywołania print (lub zabezpiecz je flagą debugowania, którą można wyłączyć).

Dla diagnostyki, która powinna pozostać w ścieżce kodu na dłużej, przejdź z print() na moduł logging. Dołącza on do każdego komunikatu poziom (debug, info, warning, error) i pozwala jednym ustawieniem wyciszyć te mniej istotne na produkcji:

import logging

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

Ustawienie poziomu loggera na logging.WARNING sprawia, że wywołania info i debug kosztują praktycznie nic (napis komunikatu nigdy nie jest budowany), bez konieczności komentowania wierszy. To czyni logging właściwym narzędziem do trwałej diagnostyki; surowe print nadaje się do tej jednorazowej.