Introduction
내가 작성한 파이썬 코드가 예상하던 대로 작동하지 않거나, 예기치 못한 오류로 자꾸 종료된다면 프로그램 내부의 지역 변수를 들여다 보고 원하는 지점에 원하는 값이 대입되고 있는 지를 한번 확인해 보면 좋을 것이다.
이를 위해서, 간단하게는 print 함수를 써서 확인이 필요한 지점마다 값을 출력하게 해 프로그램 실행 중의 변수를 확인하거나, 조그만 테스트 코드를 작성해 문제가 될 만한 상황을 재현해 볼 수 있다.
하지만 프로그램과 데이터의 규모가 커지고 복잡해질수록, 위와 같은 방법만으로는 문제의 원인을 해결하기가 여전히 어려울 수 있다.
이런 상황에서는 파이썬 디버거 (pdb) 가 유용한 해결책이 될 수 있다.
pdb - Python debugger
pdb 모듈은 파이썬을 위한 내장 대화형 디버거로, 다른 프로그래밍 언어에서와 같이 중단점 (breakpoint)을 거는 기능이나 문장 단위의 실행, 임의의 식 평가 등의 기능을 제공한다. 한번 예시와 함께 기능을 간단하게 알아보자.
Sample Code
import matplotlib.pyplot as plt
import numpy as np
def p_control(x_init, x_target, k_p):
x = x_init
x_dot = 0.0
x_ddot = 0.0
t = 0
dt = 1e-3
hist = []
while abs(x_target - x) > 1e-3 and t < 10.0:
F = - (x - x_target) * k_p
x_ddot = F - 3 * x_dot
x_dot = x_dot + x_ddot * dt
x = x + x_dot * dt
t = t + dt
hist.append([x, F, t])
return np.array(hist)
history = p_control(5.0, 0.0, 3)
plt.plot(history[:, 2], history[:, 0], label='state')
plt.plot(history[:, 2], history[:, 1], label='input')
plt.xlabel('t')
plt.legend()
plt.grid()
plt.show()
위 코드는 간단한 P 제어를 구현한 코드이다.
잘 작동하는 것을 확인할 수 있다.
중단점 추가
한번 중단점을 설정해 프로그램을 일시 중단 시켜보고 변수를 살펴 보자. 중단점을 프로그램 실행 전에 pre-define 하고 싶은 경우엔 특별하게 모듈을 임포트 할 필요 없이, 내장 함수인 breakpoint()를 원하는 위치에 추가해 주면 된다. 아래 코드와 결과를 보자.
import matplotlib.pyplot as plt
import numpy as np
def p_control(x_init, x_target, k_p):
x = x_init
x_dot = 0.0
x_ddot = 0.0
t = 0
dt = 1e-3
hist = []
while abs(x_target - x) > 1e-3 and t < 10.0:
breakpoint()
F = - (x - x_target) * k_p
x_ddot = F - 3 * x_dot
x_dot = x_dot + x_ddot * dt
x = x + x_dot * dt
t = t + dt
hist.append([x, F, t])
return np.array(hist)
>>
main.py(15)p_control()
-> F = - (x - x_target) * k_p
(Pdb)
breakpoint 함수 바로 다음 줄을 실행하기 전에 프로그램이 일시 중단되며, 프로그램을 실행시킨 터미널에서 명령어를 입력할 수 있게 된다 (Pdb로 시작하는 라인).
이렇게 breakpoint를 호출하는 기능은 기존에는 import pdb; pdb.set_trace() 와 같은 형식으로 수행할 수 있었지만, 파이썬 3.7 버전부터 모듈 임포트 없이 breakpoint() 한 줄만으로 동일한 기능을 쓸 수 있게 변경되었다. 기능상으로 완전히 동일한 듯 하다.
이렇게 대화 창이 띄워진 상태에서는 지역 변수를 이용해 식을 평가하거나 새로운 모듈 임포트, 새 객체를 생성하거나 변수의 값을 수정하는 등 굉장히 자유롭게 프로그램을 건드릴 수가 있다.
> main.py(20)p_control()
-> hist.append([x, F, t])
(Pdb) x
4.999985
(Pdb) x_dot
-0.015
(Pdb) x_ddot
-15.0
(Pdb) F = - (x - x_target) * k_p
(Pdb) import pillow
*** ModuleNotFoundError: No module named 'pillow'
(Pdb) import PIL
(Pdb)
그리고 여러 가지 명령어를 통해 프로그램의 실행을 제어할 수가 있는데, 아래에서 주요한 것들만 간단히 살펴보자.
코드 상 위치 출력
현재 어떤 위치에서 디버거가 멈춰 있는지 확인하려면, list 혹은 l 을 입력해 주면 된다.
main.py(15)p_control()
-> F = - (x - x_target) * k_p
(Pdb) list
10 dt = 1e-3
11
12 hist = []
13 while abs(x_target - x) > 1e-3 and t < 10.0:
14 breakpoint()
15 -> F = - (x - x_target) * k_p
16 x_ddot = F - 3 * x_dot
17 x_dot = x_dot + x_ddot * dt
18 x = x + x_dot * dt
19 t = t + dt
20 hist.append([x, F, t])
(Pdb)
여기서 인자 하나를 추가하면 (예: list 17) 해당 라인 넘버 주위로 11개 라인의 내용을 표시한다. 인자가 두 개라면 두 라인 넘버 사이의 모든 줄을 표시한다.
호출 스택 출력
where 혹은 w 를 입력하면 현재 위치를 포함한 프로그램의 호출 스택을 볼 수 있다.
(Pdb) where
main.py(24)<module>()
-> history = p_control(5.0, 0.0, 3)
> main.py(15)p_control()
-> F = - (x - x_target) * k_p
(Pdb)
호출 스택 영역 이동
up (u), 그리고 down (d) 를 통해 호출 스택 영역 단계를 이동해 다른 단계의 지역 변수를 확인할 수 있다.
다음 단계 실행
step (s), 그리고 next (n) 을 사용하면 현재 라인에서 벗어나 다음 줄을 실행시킨 후, 다시 디버거로 돌아올 수 있다. step은 다음 줄이 함수를 호출하는 부분이라면 해당 함수로 들어가 함수 첫 줄에서 대화형 디버거로 돌아오며, next는 같은 상황에서 호출한 함수의 리턴 이후에서 디버거로 돌아오게 된다. 비주얼 스튜디오에서의 step in과 그냥 step 정도의 차이로 느껴진다 (정확히 이름이 맞는지는 모르겠지만 비슷하게 툴이 나누어져 있었던 것 같다).
아래의 상황에서 두 기능의 차이를 한번 확인해 보자.
def sample_function():
print("Check!")
def p_control(x_init, x_target, k_p):
...
hist = []
while abs(x_target - x) > 1e-3 and t < 10.0:
...
breakpoint()
sample_function()
...
return np.array(hist)
history = p_control(5.0, 0.0, 3)
Step
(Pdb) step
--Call--
> c:\users\백종혁\pycharmprojects\blogexamples\main.py(5)sample_function()
-> def sample_function():
(Pdb)
Next
(Pdb) next
Check!
> c:\users\백종혁\pycharmprojects\blogexamples\main.py(25)p_control()
-> hist.append([x, F, t])
(Pdb)
Step은 다음 라인의 함수 첫 줄에서 디버거로 돌아오며, Next는 함수 반환 이후의 라인에서 디버거로 돌아오는 것을 확인할 수 있다.
중단점 추가/제거
- break (b) : 라인 번호를 지정해줄 시 해당 라인에 새로운 중단점을 추가. 함수 인자를 제공하면 해당 함수 첫 번째 실행 가능 문장에 중단점을 추가한다. 콜론을 이용해 다른 파일의 줄에 중단점을 추가하는 것도 가능하다!
- tbreak : break와 동일하게 사용되며, 한 번만 사용할 수 있는 일회성 중단점을 추가한다.
- clear (cl) : 인자가 없다면 모든 중단점을 제거, filename:lineno 와 같은 형식일 경우 해당 라인의 모든 중단점을 제거, 공백으로 구분된 중단점 번호 배열이 제공되면 해당 중단점들을 제거한다.
- disable : 공백으로 구분된 중단점 번호 배열로 해당 중단점들을 비활성화. (나중에 다시 활성화 가능)
- enable : 해당 중단점들을 다시 활성화
- ignore : 해당 중단점들을 일정 횟수만큼 무시하고 통과시킴
(여기에 있는 명령어들은 breakpoint 함수로 지정해 주는 중단점들에는 적용이 되지 않는 듯 하다)
이외의 디버그 제어 명령어
- return (r) : 현재 함수가 반환될 때 까지 프로그램 진행
- continue (cont, c) : 다음 중단점에 도달할 때 까지 프로그램 진행
- quit (q) : 디버거를 종료 후 프로그램 종료
포스트-모템 디버깅
마지막으로 소개할 기능은 포스트-모템 (post-mortem) 디버그 기능이다. 디버그 중 프로그램이 비정상적으로 종료되는 경우 자동으로 대화형 디버거로 이동할 수 있는데, 적절한 중단점 위치를 파악할 수 없거나 예기치 못하게 프로그램이 종료되는 경우에 유용하게 쓰일 수 있다. 다음과 같이 실행시킬 수 있다.
python -m pdb -c continue main.py
여기서 -c continue를 빼면 프로그램이 즉시 실행되는 대신에 파일의 첫 실행 라인부터 디버거가 등장한다.
한번 이 기능을 시험해 보고 마무리 하자.
# main.py
...
history = p_control(5.0, 0.0, '3')
...
위의 예시를 그대로 가져와, 세 번째 인자에 처리되지 않은 형식인 string을 넣어 오류가 발생하도록 만든 후 포스트-모템 디버그 기능을 시험해 보자.
$ python -m pdb -c continue main.py
>>
Traceback (most recent call last):
File "C:\Users\Public\Anaconda3\lib\pdb.py", line 1704, in main
pdb._runscript(mainpyfile)
File "C:\Users\Public\Anaconda3\lib\pdb.py", line 1573, in _runscript
self.run(statement)
File "C:\Users\Public\Anaconda3\lib\bdb.py", line 580, in run
exec(cmd, globals, locals)
File "<string>", line 1, in <module>
File "main.py", line 1, in <module>
import matplotlib.pyplot as plt
File "main.py", line 14, in p_control
F = - (x - x_target) * k_p
TypeError: can`t multiply sequence by non-int of type 'float'
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> main.py(14)p_control()
-> F = - (x - x_target) * k_p
(Pdb) args
x_init = 5.0
x_target = 0.0
k_p = '3'
(Pdb)
코드 실행 중 에러가 발생하고 나서 바로 대화형 디버거로 제어가 넘어가며, 명령어를 통해 바로 해당 시점에서의 지역 변수 내용을 점검할 수 있다!
이 기능은 특히 테스트 코드로 재현이 어렵고, 런타임 중에서만 발생하는 에러를 잡아낼 때 굉장히 유용하다. 한참 코드가 돌아가는 중간에 에러가 발생하는 상황에서, 변수나 조사식 프린트로만 디버그를 하려면 실행-출력 사이클을 반복하는 데 한세월이 걸리기 때문에 이런 식으로 에러 발생 시점에서부터 시작하는 디버거를 이용하면 훨씬 효율적이다. 본인도 여기 적어놓은 많고 많은 기능 중에 이 포스트-모텀 디버깅 기능을 가장 많이 이용하는 편이다.
이번에는 간단하게 이용할 수 있는 파이썬 내장 디버거 모듈의 사용법을 알아봤다. 물론 대다수의 IDE는 자체적으로 모듈을 UI로 씌워놓은 디버그 툴을 제공하기는 하지만, 이렇게 대화형 디버거를 직접 이용하는 방법을 알아두면 언젠간 도움이 될 것이다.
아래는 참고할 만한 사이트이다.
References
파이썬 디버거 도큐멘테이션
https://docs.python.org/ko/3.7/library/pdb.html#pdb.runeval