Na problemy - printuj wszystko. To sposób rozwiązywania problemów znany nam wszystkim. Mnie też mimo wszystko zdarza się to robić (Gynvael Coldwind także przyznaje, że jest to dobra metoda). Jednak czasem to za mało. Ilość danych, których potrzebujemy do weryfikacji, zaczyna być coraz dłuższa. Na takie problemy mamy Python Debugger!
Python Debugger
PDB to interaktywny debugger kodu źródłowego języka Python. Pozwala nam na ustawianie w kodzie tzw. breakpoint
czyli miejsc, w których chcemy sprawdzić, co się dzieje.
Użycie
Przypuśćmy, że mamy taki kod jak poniżej. Po uruchomieniu tego skryptu, w terminalu otrzymamy wyjątek AssertionError
. Pojawia się, gdyż logika użyta w instrukcji assert
jest niepoprawna, i w jej wyniku otrzymaliśmy False
. Wstępnie wygląda na to, że nasza funkcja zwraca niepoprawną wartość.
def lower_camel_case(string: str) -> str: words = string.split(" ") title_words = [w.title() for w in words] new_string = "".join(title_words) return new_string result = lower_camel_case("simple function name") assert result == "simpleFunctionName"
Traceback (most recent call last): File "/home/jarek/Playground/blog_entry/debug.py", line 7, in <module> assert result == "simpleFunctionName" AssertionError:
W pierwszej kolejności skorzystajmy z debuggera! Aby go wywołać, musimy w naszym kodzie zostawić tzw. pułapkę. W przypadku Pythona jest to funkcja breakpoint()
lub import pdb; pdb.set_trace()
dla wersji pythona < 3.7. Następnie uruchamiamy skrypt ponownie tak jak wcześniej.
def lower_camel_case(string: str) -> str: words = string.split(" ") title_words = [w.title() for w in words] new_string = "".join(title_words) breakpoint() return new_string result = lower_camel_case("simple function name") assert result == "simpleFunctionName"
Terminal się zatrzymał i nie wywołał wyjątku. Jednak to, co widać w terminalu, jest czymś innym niż to, do czego się przyzwyczailiśmy.
> /home/jarek/Playground/blog_entry/debug.py(6)lower_camel_case() -> return new_string (Pdb)
W pierwszej linii dostajemy informacje o pliku, w którym jesteśmy, linii, która zostanie wywołana jako następna oraz funkcja, w której ciele się znajdujemy. Następna linia to kod, który zostanie wykonany jako kolejny. Na koniec (Pdb)
, które jest znakiem zachęty. System w ten sposób oczekuje na wprowadzenie danych przez użytkownika. A co takiego użytkownik może?
Komendy
Po pierwsze komendy. Po wpisaniu help
otrzymamy całą tabelę komend. Dla użytkowników gdb
komendy te prawdopodobnie nie będą zaskoczeniem. Aby przeczytać o komendach, możemy użyć komendy help <komenda>
. Dla przykładu sprawdziliśmy, czym jest komenda pp
, która pomaga prezentować skomplikowane dane w czytelniejszy sposób.
(Pdb) help Documented commands (type help <topic>): ======================================== EOF c d h list q rv undisplay a cl debug help ll quit s unt alias clear disable ignore longlist r source until args commands display interact n restart step up b condition down j next return tbreak w break cont enable jump p retval u whatis bt continue exit l pp run unalias where Miscellaneous help topics: ========================== exec pdb (Pdb) help pp pp expression Pretty-print the value of the expression.
Python Expression
Jedna z ważniejszych rzeczy, która ma zastąpić nasze print
. Dzięki temu możemy sprawdzać zmienne, ale także wykonywać jednolinijkowy kod pythonowy. Możemy podejrzeć zmienną words
. Następnie sprawdzamy, co ukrywa się pod zmienną title_words
. Już widzimy, gdzie jest problem, jednak szukajmy dalej. Zmienna new_string
to wartość, którą zwrócimy z funkcji, i która jest niepoprawna.
(Pdb) words ['simple', 'function', 'name'] (Pdb) pp title_words ['Simple', 'Function', 'Name'] (Pdb) new_string 'SimpleFunctionName' (Pdb) words[0] 'simple' (Pdb) title_words[1:] ['Function', 'Name'] (Pdb) [words[0], *title_words[1:]] ['simple', 'Function', 'Name']
Ponieważ pdb
pozwala nam na jednolinijkowy kod Pythonowy, to możemy zacząć projektować poprawkę. Po pierwsze spróbujmy wyciąć część listy, która jest poprawna. Następnie sklejmy je z pierwszy słowem, które nie powinno być przekształcone. Wygląda na to, że znaleźliśmy miejsce problemu oraz napisaliśmy rozwiązanie.
Interpreter
Jeżeli jednak sprawdzanie danych to dla nas mało, możemy wejść do interpretera pythona, w którym możemy operować już na zmiennych czy importować biblioteki. Do tego użyjemy komendy interact
.
(Pdb) interact *interactive* >>> help Type help() for interactive help, or help(object) for help about object. >>> new_string="somethingElse" >>> new_string 'somethingElse' >>> exit Use exit() or Ctrl-D (i.e. EOF) to exit >>> now exiting InteractiveConsole... (Pdb) new_string 'SimpleFunctionName'
Aby opuścić ten widok, należy wywołać funkcje exit()
lub użyć skrótu Ctrl+D
. Pamiętajmy, że zmiany w zmiennych wykonane tam, nie wpłyną na nasz skrypt.
Wyjście
Mamy dwie metody wyjścia z debuggera w zależności od tego, jak chcemy wynik osiągnąć. Użycie exit()
spowoduje wygenerowanie wyjątku BdbQuit
.
(Pdb) exit() Traceback (most recent call last): File "/home/jarek/Playground/blog_entry/debug.py", line 8, in <module> result = lower_camel_case("simple function name") File "/home/jarek/Playground/blog_entry/debug.py", line 6, in lower_camel_case return new_string File "/home/jarek/Playground/blog_entry/debug.py", line 6, in lower_camel_case return new_string File "/home/jarek/.pyenv/versions/3.9.0a4/lib/python3.9/bdb.py", line 88, in trace_dispatch return self.dispatch_line(frame) File "/home/jarek/.pyenv/versions/3.9.0a4/lib/python3.9/bdb.py", line 113, in dispatch_line if self.quitting: raise BdbQuit bdb.BdbQuit
Aby wyjść z debuggera i kontynuować działanie naszego kodu należy użyć komendy continue
. Spowoduje ona, że nasz kod będzie kontynuować działanie - w naszym przypadku wygeneruje błąd, ponieważ kod nie został poprawiony.
(Pdb) continue Traceback (most recent call last): File "/home/jarek/Playground/blog_entry/debug.py", line 8, in <module> result = lower_camel_case("simple function name") AssertionError
Nawigacja
Największą siłą według mnie są komendy do nawigacji. Aby je pokazać, zmienimy miejsce, w którym uruchomimy nasz debugger.
Komendy te pozwalają poruszać się krokowo po naszym kodzie. Dzięki temu, mamy możliwość weryfikacji tego co dzieje się w środowisku po wywołaniu kolejnych linii kodu.
def lower_camel_case(string: str) -> str: words = string.split(" ") title_words = [w.title() for w in words] new_string = "".join(title_words) return new_string breakpoint() result = lower_camel_case("simple function name") assert result == "simpleFunctionName"
Next
Po pierwsze komendan(ext)
, która wykonuje krok w przód. W naszym przypadku breakpoint
ustawiony jest przed wywołaniem funkcji. Po wykonaniu komendy next
zostanie wywołana linia z przypisaniem wartości funkcji lower_camel_case
do zmiennej result
. Kolejny krok n
przenosi nas do linii z instrukcją assert
, a ta wywołuje wyjątek.
> /home/jarek/Playground/blog_entry/debug.py(8)<module>() -> result = lower_camel_case("simple function name") (Pdb) next > /home/jarek/Playground/blog_entry/debug.py(9)<module>() -> assert result == "simpleFunctionName" (Pdb) n Traceback (most recent call last):
Step
W naszym przypadku przyda się inny krok. Jest to step
który robi krok w przód lub do środka jeżeli kolejna komenda to funkcja. W poniższym przykładzie, gdy użyjemy tej komendy, wejdziemy do kodu funkcji lower_camel_case
. Zostanie to zasygnalizowane poprzez linię --Call--
, oraz zatrzymanie się na definicji funkcji. Robiąc kolejne kroki, możemy zauważyć, że debugger wszedł głębiej do list comprehension.
> /home/jarek/Playground/blog_entry/debug.py(8)<module>() -> result = lower_camel_case("simple function name") (Pdb) step --Call-- > /home/jarek/Playground/blog_entry/debug.py(1)lower_camel_case() -> def lower_camel_case(string: str) -> str: (Pdb) step > /home/jarek/Playground/blog_entry/debug.py(2)lower_camel_case() -> words = string.split(" ") (Pdb) step > /home/jarek/Playground/blog_entry/debug.py(3)lower_camel_case() -> title_words = [w.title() for w in words] (Pdb) --Call-- > /home/jarek/Playground/blog_entry/debug.py(3)<listcomp>() -> title_words = [w.title() for w in words] (Pdb) step > /home/jarek/Playground/blog_entry/debug.py(3)<listcomp>() -> title_words = [w.title() for w in words] (Pdb) step > /home/jarek/Playground/blog_entry/debug.py(3)<listcomp>() -> title_words = [w.title() for w in words] (Pdb) step > /home/jarek/Playground/blog_entry/debug.py(3)<listcomp>() -> title_words = [w.title() for w in words] (Pdb) step > /home/jarek/Playground/blog_entry/debug.py(3)<listcomp>() -> title_words = [w.title() for w in words] (Pdb) step --Return-- > /home/jarek/Playground/blog_entry/debug.py(3)<listcomp>()->['Simple', 'Function', 'Name'] -> title_words = [w.title() for w in words] (Pdb) step > /home/jarek/Playground/blog_entry/debug.py(4)lower_camel_case() -> new_string = "".join(title_words) (Pdb) step > /home/jarek/Playground/blog_entry/debug.py(5)lower_camel_case() -> return new_string (Pdb) step --Return-- > /home/jarek/Playground/blog_entry/debug.py(5)lower_camel_case()->'SimpleFunctionName' -> return new_string (Pdb) step > /home/jarek/Playground/blog_entry/debug.py(9)<module>() -> assert result == "simpleFunctionName" (Pdb)
Gdy step
wychodzi z funkcji, zostanie to nam zasygnalizowane poprzez linię --Return--
oraz wyświetlenia co zostało zwrócone z tej funkcji. W ten sposób widzimy, że wynikiem funkcji listcomp
jest ['Simple', 'Function', 'Name']
, a wynikiem naszej funkcji lower_camel_case
jest 'SimpleFunctionName'
.
Zaplecze
Nawigując po kodzie, można się pogubić. Aby nie skakać między debuggerem a naszym edytorem, możemy kod wyświetlić bezpośrednio w debuggerze. Służy do tego komenda list
. Wyświetla ona okolice linii, która jest w kolejce do wywołania.
Mimo że debugger pozwala nam na odczyt danych, są pewne komendy, które usprawniają ich odczyt. Zdążyliśmy sobie powiedzieć już o komendzie pp
przydatnej gdy mamy do czynienia z dużym słownikiem. Gdy nasza funkcja przyjmuje dużą ilość argumentów, dużo wygodniej jest użyć komendy args
. Wyświetli ona wszystkie argumenty funkcji, w której ciele się znajdujemy.
(Pdb) list 1 def lower_camel_case(string: str) -> str: 2 words = string.split(" ") 3 title_words = [w.title() for w in words] 4 new_string = "".join(title_words) 5 breakpoint() 6 -> return new_string 7 8 result = lower_camel_case("simple function name") 9 assert result == "simpleFunctionName" [EOF] (Pdb) args string = 'simple function name'
To na tyle, poznaliśmy debugger i podstawowe komendy, z których najczęściej korzystam w pracy zawodowej. Myślę, że taki zestaw będzie wystarczający na wszystkie przypadki, gdy print
to za mało. Podziel się w komentarzach komendami, z których Ty najczęściej korzystasz!
FAQ
Jak mogę uniknąć wielokrotnego wpisywania next
, aby przejść do kolejnych linii ? To męczące.
Właśnie dlatego, w kodzie możemy użyć wielu pułapek, i zostawiać je w miejscach, które nas interesują. Gdy użyjemy komendy c
lub continue
to kod będzie się wykonywał bez naszej kontroli do czasu, aż trafi na kolejną pułapkę.
Są dwa sposoby na ich stawianie. Jednym z nich jest użycie komendy break <numer linii>
. Dzięki temu postawimy pułapki, będąc już w debuggerze. Użycie komendy break
bez podania numeru, pozwoli wyświetlić wszystkie dotąd postawione pułapki (pułapki postawione w kodzie poprzez breakpoint()
nie zostaną tutaj doliczone). Jeżeli chcielibyśmy usunąć nasze pułapki, należy użyć komendy clear
.
> /home/jarek/Playground/akademia_python_code/2020-04/debug.py(14)<module>() -> result = lower_camel_case("simple function name") (Pdb) break 9 Breakpoint 1 at /home/jarek/Playground/akademia_python_code/2020-04/debug.py:9 (Pdb) c > /home/jarek/Playground/akademia_python_code/2020-04/debug.py(9)lower_camel_case() -> new_string = "".join(title_words) (Pdb) title_words ['Simple', 'Function', 'Name'] (Pdb) break Num Type Disp Enb Where 1 breakpoint keep yes at /home/jarek/Playground/akademia_python_code/2020-04/debug.py:9 breakpoint already hit 1 time (Pdb) clear Clear all breaks? y Deleted breakpoint 1 at /home/jarek/Playground/akademia_python_code/2020-04/debug.py:9
Drugi sposób, to wstawienie w naszym kodzie pułapek w kilku miejscach, tam gdzie chcemy. Należy pamiętać, aby je później usunąć, ale zdecydowanie pomaga to gdy robimy poprawki i uruchamiamy kilka razy nasz debugger. W przypadku komendy break
musielibyśmy za każdym razem wpisywać na nowo nasze pułapki!
def lower_camel_case(string: str) -> str: words = string.split(" ") title_words = [w.title() for w in words] new_string = "".join(title_words) breakpoint() return new_string breakpoint() result = lower_camel_case("simple function name") assert result == "simpleFunctionName"
> /home/jarek/Playground/akademia_python_code/2020-04/debug.py(15)<module>() -> result = lower_camel_case("simple function name") (Pdb) c > /home/jarek/Playground/akademia_python_code/2020-04/debug.py(11)lower_camel_case() -> return new_string (Pdb) c Traceback (most recent call last): File "/home/jarek/Playground/akademia_python_code/2020-04/debug.py", line 15, in <module> result = lower_camel_case("simple function name") AssertionError
Jak wyjść z debugera gdy mam breakpoint w pętli, a exit()
nie chce działać?
W sytuacji kryzysowej możemy skorzystać z os._exit
, które spowoduje nagłe zakończenie działania skryptu:
(Pdb) exit() Traceback (most recent call last): File "/home/jarek/Playground/blog_entry/debug.py", line 8, in <module> result = lower_camel_case("simple function name") File "/home/jarek/Playground/blog_entry/debug.py", line 6, in lower_camel_case return new_string File "/home/jarek/Playground/blog_entry/debug.py", line 6, in lower_camel_case return new_string File "/home/jarek/.pyenv/versions/3.9.0a4/lib/python3.9/bdb.py", line 88, in trace_dispatch return self.dispatch_line(frame) File "/home/jarek/.pyenv/versions/3.9.0a4/lib/python3.9/bdb.py", line 113, in dispatch_line if self.quitting: raise BdbQuit bdb.BdbQuit > /home/jarek/Playground/blog_entry/debug.py(6)lower_camel_case() -> return new_string (Pdb) import os; os._exit(0)