Python + debugger = PDB! Poszukajmy Tych Insektów

  • by

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 nprzenosi 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)