Czytanie Tracebacków – Czy To Piekło Kiedyś Się Kończy?
- Jarosław Piszczała
- 12 Mar, 2020
- 22 Oct, 2024
Podczas pisania kodu gdy zrobimy coś nie tak, nasz kod przy wywołaniu wyświetli Traceback (most recent call last)
z masą informacji. Dlaczego tak się dzieje?
Ponieważ kod Pythona nie jest kompilowany przed uruchomieniem. Ze względu na swoją dynamikę, błędy pojawiają się w trakcie działania kodu. Jak zatem naprawiać typowe problemy Pythona?
Czym jest traceback?
Traceback, czyli informacja z ostatniej transkacji. Jest to kawałek logów o ustalonej strukturze który informuje nas o błędzie, a także jego źródle w kodzie. Aby lepiej go zrozumieć, rozłożymy traceback na elementy aby zrozumieć jego konstrukcje. Przypuśćmy, że chcemy skorzystać z biblioteki Beautiful Soup
. Aby to zrobić robimy import bs4
jak poniżej:
Po wywołaniu komendy dostaniemy błąd. W tym przypadku związane jest to z brakiem możliwości zaimportowania takiego modułu o czym informuje nas wyjątek ModuleNotFoundError
widoczny poniżej w logach błędu:
Zaraz po nazwie wyjątku otrzymujemy konkretny komunikat No module named 'bs4'
. Oznacza on, że taki moduł był niemożliwy do zaimportowania. Błąd ten może pojawić się w różnych sytuacjach:
- Błędna nazwa importowanego modułu
- Brak pliku o takiej nazwie w katalogu
- Moduł nie został zainstalowany
Poza błędem, dostajemy także ważną informacje o tym, jaki kod wywołał ten błąd:
W tym przypadu jest to import bs4
. Co dla nas najistotniejsze, dostajemy także informacje w którym pliku oraz w której lini znajduje się ten kod:
Jest to pierwsza linia w pliku main.py
. Jeżeli błąd jest zagnieżdżony to konstrukcja ta pokaże nam całą drogę zagnieżdzeń błędu. Mając wszystkie te informacje nie pozostaje nam nic innego jak naprawić nasz kod.
Praktyka!
Aby zaprezentować sposób naprawy typowych Pythonowych błędów przygotowałem kawałek kodu z kilkoma błędami które popełniłem podczas implementacji. Jest to kod który ma na celu stworzyć nam API poprzez które będziemy mogli dodawać nowe kursy oraz ich moduły.
NameError
Po uruchomieniu naszego kodu otrzymujemy pierwszy błąd. Jest to NameError który informuje nas, że dany element jest nie dostępny. W tym przypadku jest to element List
.
Jest tutaj także informacja, że jest to linia 18 w naszym pliku, oraz mamy informacje o elemencie nadrzędnym tego wywołania którym jest Course
. W tym przypadku zapomniałem zaimportować element z biblioteki typing
, więc dopisanie tego naprawia nasz problem:
TypeError
Kolejny błąd to TypeError
. Wywołany jest ze względu na zły typ zmiennej. W tym przypadku kod oczekiwał zmiennej która będzie mogła być wywołana: czyli funkcji bądź lambdy. Jest to błąd troszkę ukryty w tym przypadku więc prześledźmy jego drogę. Wywołał się podczas próby użycia metody create
w klasie CourseLogic
. Wewnątrz tej metody błąd pojawia się dokładniej przy metodzie create
klasy CourseRepository
, a ostatecznie przy tworzeniu obiektu Course
. Dodatkowo mamy jeszcze informacje o metodzie __init__
tej klasy.
Tak więc podejrzewałem, że może to być związane z użyciem funkcji field
z biblioteki dataclasses
której jeszcze dobrze nie znałem. Po przejrzeniu dokumentacji okazało się, że parametr default_factory
oczekuje funkcji. Dlatego też obudowałem aktualnie istniejący kawałek kodu za pomocą lambda
i w ten sposób rozwiązaliśmy kolejny błąd.
Jednak pojawił się kolejny TypeError
. Metoda create_module
w klasie CourseLogic
. Używając funkcji replace wywołał się błąd, ponieważ Python nie jest w stanie połączyć listy z obiektem klasy Module
.
Problem okazuje się prosty do rozwiązania, wystarczy nasz obiekt obudować w jednoelementową listę. W taki oto sposób udało nam się pozbyć kolejnego błędu.
KeyError
Kolejny wyjątek który nam się pojawił to [KeyError][^keyerror]. Jest to typowy błąd przy próbie odczytu elementu ze słownika który nie istnieje. Ale prześledźmy gdzie ten problem się pojawił. Mamy metodę get
klasy CourseLogic
. Następnie w niej mamy wywołanie metody get
z klasy CourseRepository
. Ostatecznie błąd pojawia się w linii w której próbujemy odczytać wartość ze słownika spod klucza o nazwie course.uuid
. Jest to zdecydowanie mój błąd, gdyż chciałem używać do tego wartości tego atrybutu, a nie jego nazwy.
Po krótkim sprawdzeniu zdecydowałem zmienić wywołanie tej metody by zamiast f"course.uuid"
użyć po prostu course.uuid
. Innym rozwiązaniem jest także użycie metody get
na słowniku która może zwrócić domyślną wartość w przypadku gdy podanego klucza nie będzie w słowniku. W tym przypadku jednak chciałbym aby wyjątek był wywoływany.
AttributeError
Kolejny błąd z którym mamy doczynienia to AttributeError. Związany z problemem w dostępie do danego atrybutu. Idąc po nitce problem pojawia się przy próbie wyświetlenia obiektu klasy Course
. W metodzie __repr__
próbujemy odczytać atrybut id
.
Moja klasa nie posiada takiego atrybutu, posiada natomiast atrybut uuid
. I właśnie tego atrybutu chciałem użyć pisząc ten kod. A więc wprowadzamy poprawkę i w miejsce self.id
wpisujemy self.uuid
i problem rozwiązany.
Czy to meta?
Tak! W końcu udało się uruchomić nasz skrypt poprawnie, a w terminalu ujrzeliśmy obiekt klasy Course
zawierający dwa moduły.
Co dalej?
W ramach kodu postanowiłem w miejscu gdzie oczekuje błędu stworzyć dokładniejszy wyjątek. Dlatego też w pierwszym kroku zrobiłem nowy wyjątek o nazwie CourseNotFoundError
, dziedziczący po klasie Exception
.
Następnie wprowadziłem try except
do metody, w której chciałem aby błąd się faktycznie pojawiał.
Teraz gdy wywołamy ponownie błąd podając nie poprawne course_id
otrzymamy nasz CourseNotFoundError
. Ze względu na wywołanie naszego wyjątku podczas obsługi wyjątku KeyError
dostaliśmy rozwinięty traceback. Informuje on nas, że podczas gdy pojawił się wyjątek KeyError
, podczas jego obsługi pojawił się kolejny wyjątek.
W ten oto sposób Python stara się przekazać nam wystarczającą ilość informacji abyśmy mogli rozwiązać nasz problem i obsłużyć błąd który się pojawił. W powyższym przypadku możemy pozbyć się nadmiernej ilości logów poprzez zmianę logiki kodu. Tym razem najpierw pobierzemy wartość poprzez metode get
biorąc pod uwagę, że jeżeli klucz nie będzie dostępny, otrzymamy None
Następnie weryfikujemy, czy nasz obiekt nie jest przypadkiem typu None
. Jeżeli nie jest, zwracamy go z metody. W innym przypadku wywołujemy wyjątek.
W przypadku naszych wyjątków informacja pojawia się bezpośrednio w miejscu gdzie używamy instrukcji raise
aby go wywołać. Na szczęście wiemy, że to nasz błąd i oczekujemy go tutaj, więc aby błąd się nie pojawił musimy coś zrobić na wyższym poziomie. W przypadku tego kodu należałoby użyć poprawnego course_id
lub użyć try except
w momencie wywoływania metody get klasy CourseLogic
.
Q&A
Dlaczego kod nie poprawia się sam?
Najprostszą odpowiedzą byłoby: gdyby kod był wstani naprawić się sam, prawdopodobni potrafiłby także sam się napisać. Więc nie potrzebni byliby programiści, a Ci nie przeżywaliby niemiłych chwil związanych z naprawianiem występujących błędów.
Z drugiej strony komputer wie tylko, że wystąpił błąd. On nie wie w jaki sposób chcesz go rozwiązać. Na powyższych przykładach mogliśmy prześledzić naprawę kilku błędów. Niektóre z nich były dość trywialne, inne wymagały dopisania kilku linii kodu. Czytanie logów i naprawianie błędów to część pracy programisty.
Jak uchronić się od błędów?
Najlepiej ich nie robić! Są błędy które z początku będziemy popełniać. Z czasem nauczymy się jak wywoływać metody, czy pisać kod aby te błędy się nie pojawiały. W dużej mierze przed błędami może nas uchronić wyłącznie nasze doświadczenie.
Na szczęście mamy wielu pomocników którzy wspomogą nas w unikaniu problemów. Są to chociażby IDE jak PyCharm. W momencie pisania niepoprawnego kodu jest on w stanie wyłapać niektóre przypadki i wyświetlić nam błąd zanim uruchomimy kod. Mówimy na takie systemy, że są to sytemy do statycznej analizy kodu. Poza IDE możemy skorzystać także z takich narzędzi jak flake8
czy pylint
.
Co mogę jeszcze zrobić?
Typowanie. Na powyższym przykładzie użyłem dużo typowania w kodzie. Pozwala ono weryfikować podczas pisania kodu czy mamy dostęp dla danych obiektów do metod/atrybutów do których staramy się właśnie odwołać. Do analizy kodu pod względem poprawności typowania służy chociażby narzędzie mypy
.
To na tyle. Mam nadzieję, że te przykłady pozwolą Ci dużo szybciej rozwiązywać problemy w Twoim kodzie. Staraj się uczyć na błędach i pisząc kod od razu przewidywać jego zachowanie aby nie czekać, aż błąd pojawi się w uruchomionym skrypcie.