메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

한빛랩스 - 지식에 가능성을 머지하다 / 강의 콘텐츠 무료로 수강하시고 피드백을 남겨주세요. ▶︎

IT/모바일

파이썬 상호대화 디버깅

한빛미디어

|

2006-11-24

|

by HANBIT

14,266

제공: 한빛 네트워크
저자: 제레미 존스(Jeremy Jones), 전순재 역
원문: Interactive Debugging in Python

전형적으로 프로그래머는 디버거를 도구함의 "중요한" 곳에 둔다. 디스어셈블러와 네크워크 패킷 스니퍼 사이 어딘가에 말이다. 디버거를 찾는 유일한 때는 무언가 잘못되거나 깨졌는데 print 서술문 같은 표준 디버깅 테크닉으로는 (또는 좀 더 좋은 것으로, 로그 메시지로는) 문제의 근원이 드러나지 않을 때이다. 파이썬 표준 라이브러리에는 상호대화 소스 코드 디버거가 포함되어 있는데 이것은 "중요한" 상황에 잘 맞는다.

파이썬 상호대화 소스 코드 디버거는 통제된 상태로 코드를 실행한다. 코드 조각을 한 번에 한 줄씩 추적할 수 있으며, 호출 트리를 따라 오르 내릴 수 있고, 정지 점들을 설정할 수 있으며, 그리고 파이썬 쉘의 힘을 다양한 수준의 검사와 제어에 사용할 수 있다.

그래서 뭐가 그렇게 대단한가? 소스 코드를 변경하여 print 서술문들을 곳곳에 배치하고 (대부분은 print "dir>>", dir() 또는 print "variable_name>>", variable_name과 비슷하게 말이다) 다시 실행시켜 보면서 대부분의 일을 할 수 있다. 이것도 괜찮지만, 편리성과 통제에 문제가 있다.

편리성을 고려해 보면, 디버거에 뛰어 들어가 눈 앞에서 무슨 일이 일어나는지 직접 보는 것이 더 좋다. 코드를 수정하고 다시 실행하는 것보다 파이썬 프롬프트에서 바로 코드에 손을 대는 것이 훨씬 더 편리하다. 열람하는데 엄청난 시간이 드는 데이터 집합을 열람한 후에 버그가 일어난 데이터베이스 어플리케이션을 디버그하려고 한다면 어떻게 할까? 더 나쁜 경우로서, 계산 집약적인 어플리케이션에서 몇 시간이나 걸려 데이터를 처리한 후에 버그가 일어난다면 어떻게 할까? 상호대화 디버거나 print 테크닉의 디버깅을 사용하여 운 좋으면 첫 프로그램 실행에서 거의 딱 맞을 수도 있다. 그러나 사실 첫 실행에서 충분한 데이터를 모아 그 문제를 성공적으로 해결하기는 어려운 법이다. 그 문제를 해결하기 위하여 소스 코드에 여러 print 서술문을 삽입하고 여러번 실행할 필요가 있을 때 진가가 나타난다. 디버거가 있다면, 엄청난 양의 정보를 모으고 분석하며, 더 나아가 문제를 단 한번에 해결할 수 있다.

제어의 관점에서 보면, 편리성과 겹쳐지는데, 어플리케이션을 프롬프트상에서 디버깅하면, 소스 코드를 변경해서 그 코드를 실행하는 것에 비하여, 직접적으로 통제를 할 수 있다. 어떤 경우는 코드 집합에서 살아있는 객체들을 손가락으로 만지며 프롬프트를 통하여 그 객체들과 상호작용할 수 있다면 무엇이 잘못되었는지 알아내기가 더 쉽다. 특히, IPython같이 강력한 쉘을 사용하고 있다면 말이다; 상호대화 프롬프트를 사용하면 즉시, 상호대화적으로 코드 집합에 살아있는 객체들을 통제할 수 있다.

디버거 모듈의 내용

pdb 모듈에 디버거가 포함되어 있다. pdb에는 Pdb라는 클래스가 하나 있는데, 이 클래스는 bdb.Bdb에서 상속받는다. 디버거 문서에 의하면 여섯개의 함수가 있다. 이 함수들이 상호대화 디버깅 세션을 만든다:

pdb.run(statement[, globals[, locals]])
pdb.runeval(expression[, globals[, locals]])
pdb.runcall(function[, argument, ...])
pdb.set_trace()
pdb.post_mortem(traceback)
pdb.pm()

여섯 함수 모두 약간 다른 메커니즘으로 사용자를 디버거로 인도한다.

pdb.run(statement[, globals[, locals]])

pdb.run()은 디버거의 통제하에 statement 문자열을 실행한다. 전역 사전과 지역 사전은 선택적인 매개변수이다:

#!/usr/bin/env python

import pdb

def test_debugger(some_int):
    print "start some_int>>", some_int
    return_int = 10 / some_int
    print "end some_int>>", some_int
    return return_int

if __name__ == "__main__":
    pdb.run("test_debugger(0)")
    
pdb.runeval(expression[, globals[, locals]])

pdb.runeval()은 pdb.run()과 동일하다. 단 pdb.runeval()는 평가된 문자열 expression의 값을 돌려준다는 점만 제외하고 말이다:

#!/usr/bin/env python

import pdb

def test_debugger(some_int):
    print "start some_int>>", some_int
    return_int = 10 / some_int
    print "end some_int>>", some_int
    return return_int

if __name__ == "__main__":
    pdb.runeval("test_debugger(0)")

pdb.runcall(function[, argument, ...])

pdb.runcall()는 지정된 함수(function)를 호출하고 지정된 인자들을 그 함수에 건넨다:

#!/usr/bin/env python

import pdb

def test_debugger(some_int):
    print "start some_int>>", some_int
    return_int = 10 / some_int
    print "end some_int>>", some_int
    return return_int

if __name__ == "__main__":
    pdb.runcall(test_debugger, 0)
    
pdb.set_trace()

pdb.set_trace()는 실행에 마주하면 코드를 디버거에 떨구어준다:

#!/usr/bin/env python

import pdb

def test_debugger(some_int):
    pdb.set_trace()
    print "start some_int>>", some_int
    return_int = 10 / some_int
    print "end some_int>>", some_int
    return return_int

if __name__ == "__main__":
    test_debugger(0)
    
pdb.post_mortem(traceback)

pdb.post_mortem()는 지정된 traceback의 사후(postmortem) 디버깅을 수행한다:

#!/usr/bin/env python

import pdb

def test_debugger(some_int):
    print "start some_int>>", some_int
    return_int = 10 / some_int
    print "end some_int>>", some_int
    return return_int

if __name__ == "__main__":
    try:
        test_debugger(0)
    except:
        import sys
        tb = sys.exc_info()[2]
        pdb.post_mortem(tb)
        
pdb.pm()

pdb.pm()은 sys.last_traceback에 포함된 역추적의 사후 디버깅을 수행한다:

#!/usr/bin/env python

import pdb
import sys

def test_debugger(some_int):
    print "start some_int>>", some_int
    return_int = 10 / some_int
    print "end some_int>>", some_int
    return return_int

def do_debugger(type, value, tb):
    pdb.pm()

if __name__ == "__main__":
    sys.excepthook = do_debugger
    test_debugger(0)

사용례

이제 디버깅 세션에 들어가는 법을 보여주었으므로, 간단한 예를 보여줄 시간이다. 파이썬 표준 라이브러리 참조 문서에는 파이썬 디버거 명령어가 포함되어 있지만, 그 명령어들을 복습하면서 소개해 보겠다. 다음 예제 스크립트는 f1()를 호출하면서 시작하는데, 이 함수는 f2()를 호출하고, 또 함수는 f3()을 호출하며, 계속해서 f4()를 호출한다음, 다시 사슬의 처음으로 되돌아 온다. 스크립트를 실행하면 즉시 디버깅 세션으로 진입한다:

#!/usr/bin/env python

import pdb

def f1(some_arg):
    print some_arg
    some_other_arg = some_arg + 1
    return f2(some_other_arg)

def f2(some_arg):
    print some_arg
    some_other_arg = some_arg + 1
    return f3(some_other_arg)

def f3(some_arg):
    print some_arg
    some_other_arg = some_arg + 1
    return f4(some_other_arg)
    
def f4(some_arg):
    print some_arg
    some_other_arg = some_arg + 1
    return some_other_arg

if __name__ == "__main__":
    pdb.runcall(f1, 1)
    
이 조각 코드를 실행하면, 즉시 다음과 같은 (Pdb) 프롬프트를 맞이한다:

jmjones@bean:~/debugger $ python simple_debugger_example.py
> /home/jmjones/debugger/simple_debugger_example.py(8)f1()
-> print some_arg (Pdb)

명령어에 대한 주의

대부분의 디버거 명령어들은 약자가 있다. 예를 들어, 완전한 이름으로 step을 사용하거나 그의 약자인 s를 사용할 수 있다. 명령어를 소개하면, 그 약자 둘레에 괄호를 치겠다. step을 예로 들면, step을 (s)tep라는 형태로 소개할 것이다. 디버거에 관한 모든 명령어들과 그의 약자들은 위에 언급한 파이썬 표준 라이브러리 문서의 디버거 명령어 페이지에서 참조하면 된다.

먼저, (s)tep을 이용하여 f2()에 걸어 들어가, (l)ist 명령어로 내가 어디에 있는지 보고 싶었다:

(Pdb) s
1
> /home/jmjones/svn/articles/debugger/simple_debugger_example.py(9)f1()
-> some_other_arg = some_arg + 1
(Pdb) s
> /home/jmjones/svn/articles/debugger/simple_debugger_example.py(10)f1()
-> return f2(some_other_arg)
(Pdb) s
--Call--
> /home/jmjones/svn/articles/debugger/simple_debugger_example.py(12)f2()
-> def f2(some_arg):
(Pdb) s
> /home/jmjones/svn/articles/debugger/simple_debugger_example.py(13)f2()
-> print some_arg
(Pdb) l
  8         print some_arg
  9         some_other_arg = some_arg + 1
10         return f2(some_other_arg)
11
12     def f2(some_arg):
13  ->     print some_arg
14         some_other_arg = some_arg + 1
15         return f3(some_other_arg)
16
17     def f3(some_arg):
18         print some_arg
(Pdb)

step 명령어는 다음 코드 조각을 실행하고 디버거 프롬프트를 돌려준다. 실행될 다음 코드 조각이 함수 안에 있다면, 그 함수 안으로 걸어 들어간다. list 명령어는 코드에서 디버거의 현재 위치에서 위로 다섯 줄 아래로 다섯 줄을 보여준다. 위의 list 명령어는 -> 문자가 있는 13번 줄에 디버거가 있다는 것을 보여준다. step 명령어를 반복하는 대신에, 그냥 엔터를 눌러도 좋다. 이러면 앞의 명령어를 반복한다.

다음 트릭은 (b)reak 명령어를 사용하여 정지점을 f4() 함수 안에 설정하고, (c)ontinue 명령어를 사용하여 다음 정지점까지 실행하는 것이다:

(Pdb) b f4
Breakpoint 1 at /home/jmjones/svn/articles/debugger
  /simple_debugger_example.py:22
(Pdb) c
2
3
> /home/jmjones/svn/articles/debugger/simple_debugger_example.py(23)f4()
-> print some_arg
(Pdb) l
18         print some_arg
19         some_other_arg = some_arg + 1
20         return f4(some_other_arg)
21
22 B   def f4(some_arg):
23  ->     print some_arg
24         some_other_arg = some_arg + 1
25         return some_other_arg
26
27     if __name__ == "__main__":
28         pdb.runcall(f1, 1)
(Pdb)

break 명령어는 정지점을 만들어 주며 디버거는 정지점을 만나면 실행을 멈춘다. continue 명령어는 디버거에게 정지점이나 EOF를 만날 때까지 실행을 계속하라고 명령한다.

다음으로 (w)here 명령어를 제출했다:

(Pdb) where
  /usr/local/python24/lib/python2.4/bdb.py(404)runcall()
-> res = func(*args, **kwds)
  /home/jmjones/svn/articles/debugger/simple_debugger_example.py(10)f1()
-> return f2(some_other_arg)
  /home/jmjones/svn/articles/debugger/simple_debugger_example.py(15)f2()
-> return f3(some_other_arg)
  /home/jmjones/svn/articles/debugger/simple_debugger_example.py(20)f3()
-> return f4(some_other_arg)
> /home/jmjones/svn/articles/debugger/simple_debugger_example.py(23)f4()
-> print some_arg
(Pdb)

where 명령어는 스택 추적을 인쇄하여, f1()에서 f4()까지의 호출 트리를 보여준다. 이렇게 하면 함수 어떻게 호출되는지 쉽게 볼 수 있다.

다음으로 (u)p 명령어로 스택 추적을 둘러보고 list 명령어로 어디쯤 있는지 알아보았다:

(Pdb) u
> /home/jmjones/debugger/simple_debugger_example.py(20)f3()
-> return f4(some_other_arg)
(Pdb) u
> /home/jmjones/debugger/simple_debugger_example.py(15)f2()
-> return f3(some_other_arg)
(Pdb) u
> /home/jmjones/debugger/simple_debugger_example.py(10)f1()
-> return f2(some_other_arg)
(Pdb) u
> /usr/local/python24/lib/python2.4/bdb.py(404)runcall()
-> res = func(*args, **kwds)
(Pdb) u
*** Oldest frame
(Pdb) l
399             self.reset()
400             sys.settrace(self.trace_dispatch)
401             res = None
402             try:
403                 try:
404  ->                 res = func(*args, **kwds)
405                 except BdbQuit:
406                     pass
407             finally:
408                 self.quitting = 1
409                 sys.settrace(None)
(Pdb)

up 명령어는 스택 추적에서 디버거를 한 프레임 위 예전 프레임으로 이동시킨다. 이 예제에서는 제일 오래된 프레임까지 둘러보았는데, 이 프레임은 디버거 모듈중의 하나인 bdb.py이다. 그 이유는 디버거가 (bdb.py의 일부이므로) 실제로는 이 예제 스크립트를 실행하고 있기 때문이다.

(d)own 명령어로 몇 프레임 내려가 스택 추적을 탐험하였다. 이 명령어는 up 명령어의 반대이다:

(Pdb) d
> /home/jmjones/svn/articles/debugger/simple_debugger_example.py(10)f1()
-> return f2(some_other_arg)
(Pdb) d
> /home/jmjones/svn/articles/debugger/simple_debugger_example.py(15)f2()
-> return f3(some_other_arg)
(Pdb) l
10         return f2(some_other_arg)
11
12     def f2(some_arg):
13         print some_arg
14         some_other_arg = some_arg + 1
15  ->     return f3(some_other_arg)
16
17     def f3(some_arg):
18         print some_arg
19         some_other_arg = some_arg + 1
20         return f4(some_other_arg)
(Pdb)

두 프레임 아래 f2()로 이동했지만, 다른 프레임 안에 있다는 것이 실제로 무슨 뜻인가? 일 예로, 변수 두 개를 두 가지 다른 프레임에서 인쇄해 보았다:

(Pdb) print some_arg
2
(Pdb) d
> /home/jmjones/svn/articles/debugger/simple_debugger_example.py(20)f3()
-> return f4(some_other_arg)
(Pdb) l
15         return f3(some_other_arg)
16
17     def f3(some_arg):
18         print some_arg
19         some_other_arg = some_arg + 1
20  ->     return f4(some_other_arg)
21
22 B   def f4(some_arg):
23         print some_arg
24         some_other_arg = some_arg + 1
25         return some_other_arg
(Pdb) print some_arg
3
(Pdb)

f2()에서 f3()으로 내려가서 두 함수 모두에서 some_arg를 인쇄하였다. f2()에서는 2를 f3()에서는 3을 보았다. 앞 단계(step)로 나아가면 무슨 일이 일어날까? 좀 더 확실하게 보기 위하여 f2()로 다시 돌아가자:

(Pdb) u
> /home/jmjones/debugger/simple_debugger_example.py(15)f2()
-> return f3(some_other_arg)
(Pdb) l
10         return f2(some_other_arg)
11
12     def f2(some_arg):
13         print some_arg
14         some_other_arg = some_arg + 1
15  ->     return f3(some_other_arg)
16
17     def f3(some_arg):
18         print some_arg
19         some_other_arg = some_arg + 1
20         return f4(some_other_arg)
(Pdb) s
4
> /home/jmjones/debugger/simple_debugger_example.py(24)f4()
-> some_other_arg = some_arg + 1
(Pdb) l
19         some_other_arg = some_arg + 1
20         return f4(some_other_arg)
21
22 B   def f4(some_arg):
23         print some_arg
24  ->     some_other_arg = some_arg + 1
25         return some_other_arg
26
27     if __name__ == "__main__":
28         pdb.runcall(f1, 1)
[EOF]
(Pdb)

디버거가 실제로 f2() 안에 있을 경우, f3()에 아직 호출이 이루어지지 않았다면 f3()에 가 있어야 하고, 아니면 f3()이 이미 반환되었다면 f1()로 가야 한다. 그런데 f4()로 점프해 버렸다. 한 스택 추적 위 또는 아래로 항해하면 그 프레임의 지역 이름공간에 접근할 수 있지만, 이 정도면 될 듯하다.

이 정도가 본인이 자주 사용하는 명령어들이다.

고급 예제

이제 디버거가 무엇인지 어떻게 코드를 실행시키는지, 그리고 기본 명령어들이 무엇인지 알았으므로, 좀 영양가 있는 예제를 걸어볼 시간이다. 다음 코드는 텍스트 파일을 한 번에 한 줄씩 읽어 들여, 그 줄을 공백 문자를 기준으로 잘라서, 사전으로 변환해 넣는다. 문자열필드 단어 위치를 그의 키로 하고 단어 자체의 정수 값을 그런 키들에 대한 값으로 해서 말이다:

#!/usr/bin/env python

import pdb
import string
import sys

class ConvertToDict:
    def __init__(self):
        self.tmp_dict = {}
        self.return_dict = {}
    def walk_string(self, some_string):
        """주어진 텍스트 문자열을 훓어서 사전을 돌려준다.
        예외에 마추칠 경우 실체 속성의 상태를 유지한다"""
        l = string.split(some_string)
        for i in range(len(l)):
            key = str(i)
            self.tmp_dict[key] = int(l[i])
        return_dict = self.tmp_dict
        self.return_dict = self.tmp_dict
        self.reset()
        return return_dict
    def reset(self):
        """깨끗이 치운다"""
        self.tmp_dict = {}
        self.return_dict = {}
    def get_number_dict(self, some_string):
        """예외 처리를 여기에서 한다"""
        try:
            return self.walk_string(some_string)
        except:
                        # 예외에 마추치면, 예외 시점까지 백업된 tmp_dict에 의존하면 된다
            return self.tmp_dict

def main():
    ctd = ConvertToDict()
    for line in file(sys.argv[1]):
        line = line.strip()
        print "*" * 40
        print "line>>", line
        print ctd.get_number_dict(line)
        print "*" * 40
    
if __name__ == "__main__":
    #pdb.runcall(main)
    main()
    
pdb.runcall(main)를 주석처리한 것에 주목하자. 이렇게 하면 순식간에 디버거 안에 쉽게 떨굴 수 있다. 다음은 간단한 예제 입력 파일이다:

jmjones@bean:~/debugger $ cat simple_example.data
1234 2345 3456 4567
9876 8765 7654 6543

다음은 위의 파일에 대하여 스크립트를 실행한 출력결과이다:

jmjones@bean:~/debugger $ python example_debugger.py simple_example.data
****************************************
line>> 1234 2345 3456 4567
{"1": 2345, "0": 1234, "3": 4567, "2": 3456}
****************************************
****************************************
line>> 9876 8765 7654 6543
{"1": 8765, "0": 9876, "3": 6543, "2": 7654}
****************************************

이제, 다음과 같이 입력 파일이 주어진다면:

jmjones@bean:~/debugger $ cat example_debugger.data
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 10
1 2 3 4
1 2 3 4

스크립트는 이런 출력을 생산한다:

jmjones@bean:~/debugger $ python example_debugger.py example_debugger.data
****************************************
line>> 1 2 3 4 5 6 7 8 9 10
{"1": 2, "0": 1, "3": 4, "2": 3, "5": 6, "4": 5, "7": 8, "6": 7, "9": \
    10, "8": 9}
****************************************
****************************************
line>> 1 2 3 4 5 6 7 8 9 10
{"1": 2, "0": 1, "3": 4, "2": 3, "5": 6, "4": 5, "7": 8, "6": 7}
****************************************
****************************************
line>> 1 2 3 4
{"1": 2, "0": 1, "3": 4, "2": 3, "5": 6, "4": 5, "7": 8, "6": 7}
****************************************
****************************************
line>> 1 2 3 4
{"1": 2, "0": 1, "3": 4, "2": 3}
****************************************

무언가 잘못되었다. 첫줄과 두번째 줄은 같아야 하며, 세번째 줄과 네번째 줄도 같아야 한다. 어떻게 할 것인가? 무작위로 print 서술문들을 소스 파일 전체에 걸쳐 삽입할 것인가 아니면 디버거를 기동할 것인가? 디버거를 기동하는 건 어떨까? 소스 파일에서 디버거의 주석처리를 삭제하고, main() 줄을 주석처리하자. 그러면 다음과 같이:

jmjones@bean:~/debugger $ python example_debugger.py example_debugger.data
> /home/jmjones/debugger/example_debugger.py(35)main()
-> ctd = ConvertToDict()
(Pdb)

친숙한 디버거 프롬프트가 나를 반겼다. 다음은 무엇을 할 것인가? 항상 나는 내가 어디에 있는지 확인하기를 좋아한다. 그래서 list로 시작하는 것이 좋다:

(Pdb) l
30             except:
31                 #if we hit an exception, we can rely on tmp_dict
                    being a backup to the point of the exception
32                 return self.tmp_dict
33
34     def main():
35  ->     ctd = ConvertToDict()
36         for line in file(sys.argv[1]):
37             line = line.strip()
38             print "*" * 40
39             print "line>>", line
40             print ctd.get_number_dict(line)

이 시점에서, 한 번에 한 줄씩 파일을 거닐면서 하나하나 샅샅이 조사할 수도 있고, 가정을 하나 설정한 다음 그 가정을 증명하거나 부정해 볼 수 있다. 가정을 설정해 두는 것도 맹목적을 제외하는데 도움을 준다. 무엇을 제외할 수 있을지 생각해 보았다:

- get_number_dict()는 입력 문자열을 부패시키지 않는다. 왜냐하면 그대로 walk_string()에 건네기 때문이다.
- 입력 데이터는 문제가 없어 보였다. 네 줄 모두 공백으로-분리된 정수였다.

무었 때문일까? 문제는 1 번줄이나 2 번줄에서 일어나는 듯 보이며 입력 파일의 3번줄에 이르면 문제가 일어난다. 미처 주의를 기울이지 못한 것들이 무엇일까 궁금했다:

- 어쩌면 tmp_dict나 return_dict를 적절하게 리셋하지 않고 있을 수 있다.
- 어쩌면 입력 데이터를 제대로 점검하지 않아서일 수도 있다.
- 어쩌면 모든 예외를 적절하게 처리하지 않고 있을 수 있다.

14번 줄(walk_string() 메쏘드의 첫 줄)과 18번 줄(walk_string() 메쏘드에서 for 회돌이 다음의 첫 줄)에 정지점을 설정하고, 스크립트를 한 번 쭉 실행하면서, 변수들을 조사하고, 이상 증상을 관찰하기로 결정하였다.

(Pdb) b 14
Breakpoint 1 at /home/jmjones/debugger/example_debugger.py:14
(Pdb) b 18
Breakpoint 2 at /home/jmjones/debugger/example_debugger.py:18
(Pdb) c
****************************************
line>> 1 2 3 4 5 6 7 8 9 10
> /home/jmjones/svn/home/debugger/example_debugger.py(14)walk_string()
-> l = string.split(some_string)

상황을 가늠하기 위해서, 어디에 있는지 리스트(list) 해 보았다:

(Pdb) l
  9             self.tmp_dict = {}
10             self.return_dict = {}
11         def walk_string(self, some_string):
12             """walk given text string and return a dictionary.
13             Maintain state in instance attributes in case
                we hit an exception"""
14 B->         l = string.split(some_string)
15             for i in range(len(l)):
16                 key = str(i)
17                 self.tmp_dict[key] = int(l[i])
18 B           return_dict = self.tmp_dict
19             self.return_dict = self.tmp_dict

나는 14번 줄에 있으며, 또 14번 줄과 18번 줄에 정지점이 설정되어 있다. 혹시나 하는 마음에, some_string에 무엇이 있는지 점검했다. 비록 그 값은 continue 명령어 다음에 인쇄되었지만 말이다:

(Pdb) print some_string
1 2 3 4 5 6 7 8 9 10

continue 명령어로 다음 정지점인 18번 줄에 내려왔다:

(Pdb) c
> /home/jmjones/svn/articles/debugger/example_debugger.py(18)walk_string()
-> return_dict = self.tmp_dict
이 시점에서, self.tmp_dict에는 가져야할 모든 것이 확보된다:

(Pdb) print self.tmp_dict
{"1": 2, "0": 1, "3": 4, "2": 3, "5": 6, "4": 5, "7": 8, "6": 7, "9": \
    10, "8": 9}
이것은 이상이 없어 보였다. 계속해서 다음 입력 줄을 인쇄해 보았다:

(Pdb) c
{"1": 2, "0": 1, "3": 4, "2": 3, "5": 6, "4": 5, "7": 8, "6": 7, "9": \
    10, "8": 9}
****************************************
****************************************
line>> 1 2 3 4 5 6 7 8 9 10
> /home/jmjones/svn/articles/debugger/example_debugger.py(14)walk_string()
-> l = string.split(some_string)
(Pdb) print some_string
1 2 3 4 5 6 7 8 9 10

입력 줄은 이상이 없어 보였다. 디버거가 18번 줄에 설정된 정지점에서 멈추어야 한다는 것을 주의하면서, 계속 진행했다. 두 가지 진단을 수행하여 모든 일이 제대로 보이는지 알아 볼 수 있었다:

(Pdb) c
{"1": 2, "0": 1, "3": 4, "2": 3, "5": 6, "4": 5, "7": 8, "6": 7}
****************************************
****************************************
line>> 1 2 3 4
> /home/jmjones/svn/articles/debugger/example_debugger.py(14)walk_string()
-> l = string.split(some_string)

어라? 18번 줄이 아니라 14번 줄로 되돌아가, 다음 데이터 입력줄에 있었다. 이것은 인터프리터가 18번줄에 이르기 전에 반환되었다는 뜻이며, 그래서 데이터 파일로부터 두 번째 데이터 줄을 완전히 처리하지 못했다. 문제가 어디에 있는지 발견하기는 했지만, 무었때문에 그런지는 알아내지 못했다. 다시 한번 실행해야 했는데, 이 번에는 좀 더 신중을 기하면서, 알아내려고 했다. 다행스럽게도, 이번 실행에서 다음 실행때 도움을 받을 약간의 정보를 더 얻을 수 있었다.

walk_string()이 18번 줄에 이르기 전에 반환되었다면, 적절하게 reset()을 수행하지 못했을 것이다. 그렇다면 self.return_dict와 self.tmp_dict 안에는 무엇이 있을까?

(Pdb) print self.return_dict
{}
(Pdb) print self.tmp_dict
{"1": 2, "0": 1, "3": 4, "2": 3, "5": 6, "4": 5, "7": 8, "6": 7}

이것이 바로 3번 데이터 줄에 대한 반환사전에 너무 정보가 많은 이유이다; 프로그램에서는 반환 사전과 함께 맛이 간 데이터 줄로부터 만든 self.tmp_dict 사전이 함께 따라 다닌다. 데이터 파일의 2번 줄을 처리할 때 줄 14와 줄 18 사이에서 문제가 일어났다는 것을 확신할 수 있었다. 단, 왜 그런지는 몰랐다--아직까지는 말이다.

인터프리터가 14번 줄과 18 번 줄에서 반환되는 유일한 이유는 예외에 마추쳤기 때문이다. 디버거는 두 개의 매력적인 사후 함수가 있기 때문에, 나의 스크립트에서 하나를 사용하도록 고쳐서, 어디에서 멈추는지 알아보기 위해 그 함수를 기동시켰고, 거기에서부터 디버깅을 시작했다. 다음은 변경된 소스 파일이다; 특히 get_number_dict() 메쏘드 안의 try/except 블록에 주목하자:

#!/usr/bin/env python

import pdb
import string
import sys

class ConvertToDict:
    def __init__(self):
        self.tmp_dict = {}
        self.return_dict = {}
    def walk_string(self, some_string):
        """walk given text string and return a dictionary.
        Maintain state in instance attributes in case we hit an exception"""
        l = string.split(some_string)
        for i in range(len(l)):
            key = str(i)
            self.tmp_dict[key] = int(l[i])
        return_dict = self.tmp_dict
        self.return_dict = self.tmp_dict
        self.reset()
        return return_dict
    def reset(self):
        """clean up"""
        self.tmp_dict = {}
        self.return_dict = {}
    def get_number_dict(self, some_string):
        """do super duper exception handling here"""
        try:
            return self.walk_string(some_string)
        except:
            #modified exception handler - drop us into a debugger
            tb = sys.exc_info()[2]
            pdb.post_mortem(tb)
            #if we hit an exception, we can rely on tmp_dict
                        being a backup to the point of the exception
            return self.tmp_dict

def main():
    ctd = ConvertToDict()
    for line in file(sys.argv[1]):
        line = line.strip()
        print "*" * 40
        print "line>>", line
        print ctd.get_number_dict(line)
        print "*" * 40
    
if __name__ == "__main__":
    main()
    
get_number_dict() 메쏘드의 예외 절에 사후 디버거를 기동하기로 결정했다. 왜냐하면 get_number_dict()는 walk_string() 메쏘드에서 예외가 일어나야할 곳에 가장 근접한 예외 처리자이기 때문이다. 다음은 상황에 맞게 바로 다음에서 실행하여 리스트(list)로 나타낸 결과이다:

jmjones@bean:~/debugger $ python example_debugger_pm.py example_debugger.data
****************************************
line>> 1 2 3 4 5 6 7 8 9 10
{"1": 2, "0": 1, "3": 4, "2": 3, "5": 6, "4": 5, "7": 8, "6": 7, "9": \
    10, "8": 9}
****************************************
****************************************
line>> 1 2 3 4 5 6 7 8 9 10
> /home/jmjones/debugger/example_debugger_pm.py(17)walk_string()
-> self.tmp_dict[key] = int(l[i])
(Pdb) list
12             """walk given text string and return a dictionary.
13             Maintain state in instance attributes in case
                we hit an exception"""
14             l = string.split(some_string)
15             for i in range(len(l)):
16                 key = str(i)
17  ->             self.tmp_dict[key] = int(l[i])
18             return_dict = self.tmp_dict
19             self.return_dict = self.tmp_dict
20             self.reset()
21             return return_dict
22         def reset(self):

이것으로 앞의 나의 의심이 확고해졌다. for 회돌이 안에서 예외를 일으키고 있었다. 이제 목표는 어떤 예외가 일어나고 왜 일어나는지 알아내는 것이다:

(Pdb) for e in sys.exc_info(): print "EXCEPTION>>>", e
EXCEPTION>>> exceptions.AttributeError
EXCEPTION>>> Pdb instance has no attribute "do_for"
EXCEPTION>>>

어라? 코드는 do_for() 메쏘드를 호출조차 하지 않으며 do_for 속성에 접근조차 시도하지 않는다. 역추적 모듈로 약간만 들여다 보면 무슨 일이 일어나는지 틀림없이 알 수 있을 것이다:

(Pdb) import traceback
(Pdb) traceback.print_stack()
  File "example_debugger_pm.py", line 48, in ?
    main()
  File "example_debugger_pm.py", line 44, in main
    print ctd.get_number_dict(line)
  File "example_debugger_pm.py", line 33, in get_number_dict
    pdb.post_mortem(tb)
  File "/usr/local/python24/lib/python2.4/pdb.py", line 1009, in post_mortem
    p.interaction(t.tb_frame, t)
  File "/usr/local/python24/lib/python2.4/pdb.py", line 158, in interaction
    self.cmdloop()
  File "/usr/local/python24/lib/python2.4/cmd.py", line 142, in cmdloop
    stop = self.onecmd(line)
  File "/usr/local/python24/lib/python2.4/cmd.py", line 218, in onecmd
    return self.default(line)
  File "/usr/local/python24/lib/python2.4/pdb.py", line 167, in default
    exec code in globals, locals
  File "", line 1, in ?
  
기본적으로, 파이썬 인터프리터는 실행중인 코드가 일으키는 예외를 모두 추적유지한다; 실행 코드가 예외를 때릴 때 최종 역추적을 저장한다. 디버거는 그냥 인터프리터가 실행하는 또다른 코드 조각일 뿐이다. 사용자가 디버거 명령어를 먹이면, 그런 명령어들은 디버거 자체 안에서 예외를 일으킬 수도 있다. 이 경우, 디버거에 for 서술문을 주었다. 제일 먼저 do_for() 메쏘드를 호출하여 디버거 for 명령어를 실행하려고 시도하였다. 그 때문에 위의 예외가 일어났다. 디버거 예외같은 것들은 잠재적으로 예상치 못한 예외와 역추적으로 인터프리터를 오염시킬 수도 있다. 이는 자신의 코드만을 디버그하고 싶은 사용자로서 정말로 보고 싶지 않은 것이다. 여기에서 보여준 바와 같이 인터프리터 안에서 디버그될 코드와 디버거 코드를 결합해 사용하는 것보다 디버거를 따로 만드는 것이 더 좋은 방법일 수도 있다. 그러나 파이썬 팀이 만들어낸 아주 복잡하고 교묘한 코드는 예상과는 다르게 잘 작동한다.

(q)uit 명령어로 끝내고, 스크립트를 다시 기동함으로써 디버거를 재시동하였다:

(Pdb) q
{"1": 2, "0": 1, "3": 4, "2": 3, "5": 6, "4": 5, "7": 8, "6": 7}
****************************************
****************************************
line>> 1 2 3 4
{"1": 2, "0": 1, "3": 4, "2": 3, "5": 6, "4": 5, "7": 8, "6": 7}
****************************************
****************************************
line>> 1 2 3 4
{"1": 2, "0": 1, "3": 4, "2": 3}
****************************************
jmjones@bean:~/debugger $ python example_debugger_pm.py example_debugger.data
****************************************
line>> 1 2 3 4 5 6 7 8 9 10
{"1": 2, "0": 1, "3": 4, "2": 3, "5": 6, "4": 5, "7": 8, "6": 7, "9": \
    10, "8": 9}
****************************************
****************************************
line>> 1 2 3 4 5 6 7 8 9 10
> /home/jmjones/debugger/example_debugger_pm.py(17)walk_string()
-> self.tmp_dict[key] = int(l[i])
(Pdb)

이제, 다음 줄에서 예외를 보여주기 위하여 명령어를 줄 수 있다:

(Pdb) !for e in sys.exc_info(): print "EXCEPTION", e
EXCEPTION exceptions.ValueError
EXCEPTION invalid literal for int(): 9
EXCEPTION

이것은 앞의 명령어와 똑 같다. 단 앞에다 !를 두어서 디버거에게 이것은 디버거 명령어가 아니라, 평가가 필요한 파이썬 코드라는 사실을 알려주었을 뿐이다. 디버거는 이것이 평가가 필요한 파이썬 코드라는 것을 알기 때문에, do_for() 메쏘드를 실행하고 예외를 발생시키려고 시도하지 않는다.

self.tmp_dict[key] = int(l[i]) 줄에서 ValueError 예외가 일어나는데 이는 "9"를 정수로 변환할 수 없기 때문이다 - 정말로 기묘하다. 그렇지만, 어떤 경우에는 보이는 그대로가 정확한 것은 아닐 수도 있다. 정확하게 입력은 무었일까? 다음을 보자:

(Pdb) !print l[i]
9

나에게는 아주 정상처럼 보인다. l[i]가 인쇄(print)되지 않도록 하면, 무슨 일이 일어날까?

(Pdb) !l[i]
"9\x089"

의문의 거의 풀렸다. 입력 데이터에 스스로를 가리는 묘한 값들이 들어 있었다. some_string에도 똑 같이 해 보았다 (데이터 파일에서 읽어 들인 한 줄):

(Pdb) !print some_string
1 2 3 4 5 6 7 8 9 10

이것도 역시 아주 정상처럼 보였다. 다음은 인쇄(print)하지 않은 것이다:

(Pdb) !some_string
"1 2 3 4 5 6 7 8 9\x089 10"

\x08 문자는 \b, 즉 백스페이스이다. 그래서 코드가 입력 줄을 인쇄할 때, 1 2 3 4 5 6 7 8 9가 인쇄되고, 9 위에 백스페이스가 인쇄되며, 그리고 9 10이 인쇄된다. 파이썬에게 입력 줄의 값이 무엇인지 물어보면, 문자열 값을 보여준다--출력이 불가능한 문자들은 16진 값으로 해서 말이다.

ValueError 예외는 이 상황에서 완전히 예측이 가능하다. 다음이 디버거 프롬프트에서, 같은 종류의 문자열을 정수 값으로 얻으려고 시도해 본 결과이다:

(Pdb) int("9\b9")
*** ValueError: invalid literal for int(): 9

예제 코드에서의 문제점은 두 가지로 요약된다:

- 예외를 너무 높은 수준에서 처리한다.
- 코드가 예외에 마주칠 때, 제대로 청소하지 못한다.

첫 항목 (부적절한 예외 처리) 때문에 사전에 훼손된 입력 줄의 일부가 담겼다. 키가 0에서 7까지인 사전이 생성되었다.

두 번째 항목 (부적절한 청소) 때문에 코드는 다음 줄(입력 데이터 파일의 3번줄) 대신에 훼손된 줄(입력 데이터 파일의 2번줄)로부터 존재하는 사전을 사용했다. 그 때문에 앞의 1 2 3 4 줄에서 사전의 키가 0에서 3까지 아닌 0에서 7까지 되었다. 흥미롭게도, 코드가 문자열을 정수로 변환하는 예외를 적절하게 처리했다면, 청소 작업은 논의 거리가 되지 않았을 것이다.

다음은 개선된 코드이다. 재앙적인 에러가 일어날 경우 예외를 더 잘 처리하고, 청소를 더 잘한다.:

#!/usr/bin/env python

import pdb
import string
import sys

class ConvertToDict:
    def __init__(self):
        self.tmp_dict = {}
        self.return_dict = {}
    def walk_string(self, some_string):
        """walk given text string and return a dictionary.
        Maintain state in instance attributes in case we hit an exception"""
        l = string.split(some_string)
        for i in range(len(l)):
            key = str(i)
            try:
                self.tmp_dict[key] = int(l[i])
            except ValueError:
                self.tmp_dict[key] = None
        return_dict = self.tmp_dict
        self.return_dict = self.tmp_dict
        self.reset()
        return return_dict
    def reset(self):
        """clean up"""
        self.tmp_dict = {}
        self.return_dict = {}
    def get_number_dict(self, some_string):
        """do slightly better exception handling here"""
        try:
            return self.walk_string(some_string)
        except:
            #if we hit an exception, we can rely on tmp_dict
                        being a backup to the point of the exception
            return_dict = self.tmp_dict
            self.reset()
            return return_dict

def main():
    ctd = ConvertToDict()
    for line in file(sys.argv[1]):
        line = line.strip()
        print "*" * 40
        print "line>>", line
        print ctd.get_number_dict(line)
        print "*" * 40
    
if __name__ == "__main__":
    main()
    
실행한 출력 결과는 다음과 같다:

jmjones@bean:~/debugger $ python example_debugger_fixed.py example_debugger.data
****************************************
line>> 1 2 3 4 5 6 7 8 9 10
{"1": 2, "0": 1, "3": 4, "2": 3, "5": 6, "4": 5, "7": 8, "6": 7, "9": \
    10, "8": 9}
****************************************
****************************************
line>> 1 2 3 4 5 6 7 8 9 10
{"1": 2, "0": 1, "3": 4, "2": 3, "5": 6, "4": 5, "7": 8, "6": 7, "9": \
    10, "8": None}
****************************************
****************************************
line>> 1 2 3 4
{"1": 2, "0": 1, "3": 4, "2": 3}
****************************************
****************************************
line>> 1 2 3 4
{"1": 2, "0": 1, "3": 4, "2": 3}
****************************************

훨씬 더 좋아 보인다. 스크립트가 문자열을 정수로 변환하지 못하면, 사전에 None을 넣는다.

맺는 말

파이썬 디버거는 문제를 찾아 내기 위한 노력이 모두 수포로 돌아갈 때 없어서는 안될 도구이다. 매일 사용하는 도구는 아니지만, 필요한 순간, 꼭 있어야 하는 도구이다.

제레미 존스(Jeremy Jones)는 Weather Channel에서 소프트웨어 품질 보증 엔지니어로 일하는 스크립트 전문가이다.
TAG :
댓글 입력
자료실

최근 본 상품0