디버깅

프로그램에서는 서로 다른 종류의 오류들이 발생할 수 있는데, 더 빨리 찾아내기 위해서는 분류하는 것이 도움이됩니다:

  • 문법 오류(syntax error)는 원시 코드(source code)를 바이트 코드(byte code)로 번역할 때 파이썬이 만들어냅니다. 보통 프로그램의 문법에 뭔가 잘못된 것이 있다는 것을 가리킵니다. 예: def 문의 끝에 콜론을 빼면 약간 사족 같은 메시지 SyntaxError: invalid syntax 가 나옵니다.
  • 실행시간 오류(runtime error)는 프로그램이 실행되는 도중 뭔가 잘못될 때 인터프리터가 만듭니다. 대부분의 실행시간 오류 메시지들은 어디에서 오류가 발생했으며 무슨 함수를 실행 중이었는지에 대한 정보를 포함하고 있습니다. 예: 무한 재귀는 결국 실행시간 오류 “maximum recursion depth exceeded”를 유발합니다.
  • 의미 오류(semantic error)는 오류 메시지를 만들어내는 일 없이 실행되지만 올바른 일을 하지 않는 프로그램에 관한 문제입니다. 예: 여러분이 기대하는 순서대로 표현식이 계산되지 않을 수 있고, 올바르지 않은 결과를 내게 됩니다.

디버깅의 첫 단계는 여러분이 다루고 있는 것이 어떤 종류인지 알아내는 것입니다. 다음 절들은 오류 유형에 따라 구성되어 있기는 하지만, 어떤 기술들은 한가지 이상의 상황에 적용될 수 있습니다.

문법 오류

문법 오류는 일단 무엇인지 알아내기만 하면 고치기 쉬운 편입니다. 불행히도, 오류 메시지는 종종 도움이 안됩니다. 가장 흔한 메시지는 SyntaxError: invalid syntax 와 SyntaxError: invalid token 인데, 둘 다 아주 유익하지는 않습니다.

반면에, 메시지는 문제가 발생한 프로그램상의 위치를 알려줍니다. 실제로는, 파이썬이 문제를 인지한 지점을 알려주는데, 오류가 있는 장소와 꼭 일치하는 것은 아닙니다. 때때로 오류는 오류 메시지의 위치보다 앞에, 종종 바로 앞줄에, 있습니다.

여러분이 프로그램을 점진적으로 만든다면, 오류가 어디에 있는지에 대한 감이 있게 마련입니다. 여러분이 추가한 마지막 줄입니다.

여러분이 코드를 책에서 복사하는 중이라면, 아주 조심스럽게 여러분의 코드를 책의 코드와 비교하는 것으로 시작하세요. 모든 글자를 검사하세요. 동시에, 책이 잘못될 수도 있음을 기억하세요, 그래서 문법 오류처럼 보이는 것을 발견한다면, 실제로 그런 것일 수 있습니다.

여기 흔한 문법 오류를 피하는 몇 가지 방법이 있습니다:

  1. 변수 이름으로 파이썬 예약어를 사용하고 있지 않은지 확인하세요.
  2. for, while 과 같은 모든 복문의 헤더 끝에 콜론이 있는지 검사하세요.
  3. 코드의 모든 문자열들이 상응하는 인용부호를 갖고 있는지 확실히 하세요.
  4. 삼중 인용부호를 사용한 다중라인 문자열이 있을 경우, 문자열이 올바르게 끝났는지 확실히 하세요. 끝나지 않은 문자열은 프로그램의 끝에서 invalid token 오류를 만들거나, 다음 문자열이 올 때까지 프로그램의 남은 부분을 문자열로 취급할 수 있습니다. 두 번째 경우, 오류 메시지를 전혀 만들지 않을 수도 있습니다!
  5. 닫히지 않은 오프닝(opening) 연산자—(, {, [—는 파이썬으로 하여금 현재 문장을 다음 줄까지 연장하도록 만듭니다. 일반적으로, 다음 줄에 가는 즉시 오류가 발생합니다.
  6. 고전적인 경우로, 조건식에 == 대신 =를 사용하지 않았는지 검사하세요.
  7. 들여쓰기를 검사해서 올바르게 정렬되어 있는지 확실히 하세요. 파이썬이 스페이스와 탭을 다룰 수 있습니다만, 섞어 쓴다면 문제를 일으킬 수 있습니다. 이 문제를 피하는 가장 좋은 방법은 파이썬을 알고 일관된 들여쓰기를 만들어내는 텍스트 편집기를 사용하는 것입니다.

아무 것도 통하지 않는다면, 다음 절로 가세요...

계속 바꾸는데도 차이가 보이지 않아요.

만약 인터프리터가 오류가 있다고 하는데 여러분은 볼 수 없다면, 여러분과 인터프리터가 같은 코드를 보고 있지 않기 때문일 수 있습니다. 프로그래밍 환경을 검사해서 여러분이 편집하고 있는 프로그램이 파이썬이 실행하려는 것인지 확실히 하세요.

확신할 수 없다면, 프로그램의 시작부분에 뻔하고 고의적인 문법 오류를 집어 넣으세요. 이제 다시 실행하세요. 만약 인터프리터가 새 오류를 발견하지 못한다면, 여러분은 새 코드를 실행하고 있지 않습니다.

몇 가지 가능한 원인이 있습니다:

  • 파일을 편집한 후 다시 실행하기 전에 저장하는 것을 잊었습니다. 어떤 프로그래밍 환경들은 알아서 합니다만, 어떤 것들은 그렇지 않습니다.
  • 파일의 이름을 바꿨습니다만, 아직 예전 이름으로 실행하고 있습니다.
  • 개발 환경의 뭔가가 잘못 설정되어 있습니다.
  • 모듈을 작성 중이고 import 를 사용하고 있다면, 모듈에 표준 파이썬 모듈들과 같은 이름을 주지 않았는지 확인하세요.
  • 모듈을 읽기 위해 import를 사용하고 있다면, 수정된 파일을 읽기 위해서는 인터프리터를 다시 시작시키거나 reload를 사용해야 함을 기억하세요. 모듈을 다시 들여오기 하면, 아무것도 하지 않습니다.

막혀서 무슨 일이 일어나고 있는지 이해할 수 없다면, 한가지 방법은 “Hello, World!” 같은 새 프로그램으로 시작해서, 실행됨이 확인된 프로그램을 가질 수 있는지 확인하는 것입니다. 그런 다음 원래 프로그램의 조각들을 새 프로그램에 조금씩 붙이세요.

실행시간 오류

일단 여러분의 프로그램이 문법적으로 올바르다면, 파이썬은 컴파일 해서 최소한 실행을 시작할 수는 있습니다. 뭐가 잘못될 수 있을까요?

프로그램이 아무것도 하지 않네요.

이 문제는 흔히 여러분의 파일이 함수와 클래스들로 구성되지만 실제로는 아무것도 호출하지 않을 때 발생합니다. 이 모듈을 클래스와 함수를 공급하기 위해 들여오기 하도록 계획하는 것뿐이라면 의도적일 수 있습니다.

의도적이지 않다면, 실행을 시작하도록 함수를 호출했는지 확인하거나, 대화형 프롬프트에서 실행하세요. 밑에 오는 “실행 흐름(Flow of Execution)”절도 보세요.

프로그램이 멈췄어요.

프로그램이 정지한 채로 아무것도 안 하는 듯이 보이면, “멈춘(hang)”것입니다. 종종 무한 순환이나 무한 재귀에 빠진 것을 뜻합니다.

  • 여러분이 문제로 지목하는 특정 순환이 있는 경우라면, 순환 직전에 “순환에 들어갑니다” 라고 말하는 print 문을 넣고, 직후에 “순환에서 나옵니다” 를 넣으세요.

    프로그램을 실행하세요. 첫 번째 메시지는 나오지만 두 번째는 아니라면, 무한 순환에 걸린 것입니다. 아래의 “무한 순환” 절로 가세요.

  • 대부분의 경우, 무한 재귀는 프로그램이 한동안 실행한 뒤 “RuntimeError: Maximum recursion depth exceeded” 오류를 일으키게 합니다. 이런 일이 일어나면, 아래의 “무한 재귀” 절로 가세요.

    이 오류가 발생하지는 않았지만 재귀적인 메쏘드나 함수와 관련된 문제가 의심스러운 경우도, “무한 재귀” 절에 나오는 기술들을 사용할 수 있습니다.

  • 이들 중 아무것도 통하지 않는다면, 다른 순환과 다른 재귀적 함수와 메쏘드들을 검사하기 시작하세요.

  • 이마저도 통하지 않는다면, 여러분이 프로그램의 실행흐름을 이해하지 못하는 것일 수 있습니다. 아래의 “실행 흐름”절로 가세요.

무한 순환

무한 순환이 있고, 어떤 순환이 문제를 일으키는지 안다고 생각한다면, 조건식에 나오는 변수의 값과 조건식의 값을 인쇄하는 print 문을 순환의 끝에 넣으세요.

예를 들어:

while x > 0 and y < 0 :
    # do something to x
    # do something to y

    print "x: ", x
    print "y: ", y
    print "condition: ", (x > 0 and y < 0)

이제 프로그램을 실행하면, 매 순환 마다 세 줄의 출력을 보게 됩니다. 마지막 순환에서 조건은 반드시 false가 되어야 합니다. 순환이 계속 실행되면, x 와 y 의 값들을 볼 수 있고, 왜 올바르게 갱신되지 않는지 이해할 수 있을 겁니다.

무한 재귀

대부분의 경우, 무한 재귀는 프로그램이 한동안 실행한 후 Maximum recursion depth exceeded 오류를 일으키게 합니다.

함수나 메쏘드가 무한 재귀를 일으킨다고 의심한다면, 기저 사례가 있는지 확인하는 것으로 시작하세요. 달리 표현하면, 함수나 메쏘드가 재귀적 호출 없이 복귀할 수 있는 조건이 반드시 있어야 합니다. 그렇지 않다면, 알고리즘을 다시 생각하고 기저 사례를 확인할 필요가 있습니다.

기저 사례가 있지만 프로그램이 그 곳에 도달하지 못하는 것으로 보인다면, 함수나 메쏘드의 처음에 매개변수들을 인쇄하는 print 문을 넣으세요. 이제 프로그램을 실행하면 함수나 메쏘드가 호출될 때마다 몇 줄의 출력을 보게 되고, 매개변수들을 볼 것입니다. 만약 매개변수들이 기저 사례를 향해 움직이지 않는다면, 왜 그런지에 대한 아이디어들을 얻게 될 것입니다.

실행 흐름

실행 흐름이 어떻게 움직이고 있는지 확신할 수 없다면, 각 함수들의 처음에 “entering 함수 foo에 들어갑니다” 같은 메시지를 출력하는 print 문을 넣으세요(foo는 함수의 이름입니다).

이제 프로그램을 실행하면, 호출되는 대로 각 함수들의 자취를 인쇄합니다.

프로그램을 실행하면 예외가 발생해요.

실행도중 뭔가 잘못되면, 파이썬은 예외의 이름과 문제가 발생한 프로그램의 줄과 트레이스백(traceback)을 포함한 메시지를 인쇄합니다.

트레이스백은 현재 실행중인 함수와 이를 호출한 함수와 다시 이를 호출함 함수, 등등을 확인시켜줍니다. 달리 표현하면, 현재의 위치에 이르는 함수 호출의 시퀀스를 추적합니다. 이 호출들 각각이 발생한 파일에서의 줄 번호도 포함하고 있습니다.

첫 번째 단계는 오류가 발생한 프로그램의 위치를 검사해서, 무슨 일이 일어났는지 이해할 수 있는지 보는 것입니다. 이것들이 가장 흔한 몇 가지 실행시간 오류들입니다:

NameError:
현재 환경에서 존재하지 않는 변수를 사용하려고 하고 있습니다. 지역 변수는 지역적임을 기억하세요. 정의된 함수 밖에서 참조할 수 없습니다.
TypeError:

가능한 원인들이 여러 가지 있습니다:

  • 값을 부적절하게 사용하려고 합니다. 예: 정수 외의 것을 문자열, 리스트, 튜플 등의 지수로 사용하려는 것.
  • 포맷 문자열과 변환을 위해 전달된 항목들 간의 불일치가 있습니다. 항목의 개수가 일치하지 않거나, 부적절한 변환이 호출된 경우 둘 다 가능합니다.
  • 함수나 메쏘드로 잘못된 개수의 인자를 전달하고 있습니다. 메쏘드의 경우 메쏘드 정의를 살펴서 첫 번째 매개변수가 self 인지 검사하세요. 그런 다음 메쏘드 호출을 살피세요; 올바른 형의 객체에 대해 메쏘드를 호출하고 있고 다른 인자들을 올바로 제공하고 있는지 확인하세요.
KeyError:
딕셔너리에 없는 키로 딕셔너리의 요소에 접근하려는 중입니다.
AttributeError:

존재하지 않는 애트리뷰트나 메쏘드에 접근하려는 중입니다. 철자를 검사하세요! dir 을 사용해서 존재하는 애트리뷰트들을 나열할 수 있습니다.

만약 AttributeError 가 객체가 NoneType 이라고 한다면, 객체가 None 이라는 뜻입니다. 한가지 흔한 이유는 함수에서 값을 반환하는 것을 잊는 것입니다; return 문 없이 함수의 끝에 도달하면, None을 반환합니다. 또 하나 흔한 이유는 None을 돌려주는, sort 같은, 리스트 메쏘드의 결과를 사용하는 것입니다.

IndexError:
리스트, 문자열, 튜플 등에 사용하는 지수가 길이 빼기 하나보다 큽니다. 오류가 발생한 곳 바로 앞에 지수의 값과 시퀀스의 길이를 표시하는 print 문을 넣으세요. 시퀀스는 올바른 길이를 갖고 있습니까? 지수는 올바른 값을 갖고 있습니까?

파이썬 디버거(debugger) (pdb)는 예외들을 찾아내는데 유용한데 오류 직전의 프로그램 상태를 검사할 수 있게 해주기 때문입니다. http://docs.python.org/2/library/pdb.html 에서 pdb 에 대한 내용을 읽을 수 있습니다.

너무 많은 print 문을 넣어서 출력에 파묻혔어요.

디버깅에 print 문을 사용하는 것의 한가지 문제는, 출력에 묻혀버릴 수 있다는 것입니다. 두 가지 해결책이 있습니다: 출력을 단순화하거나 프로그램을 단순화하세요.

출력을 단순화하기 위해서는, 도움이 안 되는 print 문들을 지우거나 주석 처리할 수 있습니다, 또는 출력들을 묶거나 이해하기 쉽게 포맷할 수 있습니다.

프로그램을 단순화하기 위해서는, 여러 가지를 할 수 있습니다. 첫째로, 프로그램이 다루는 문제의 규모를 줄이세요. 예를 들어, 리스트를 검색한다면, 작은 리스트를 검색하세요. 프로그램이 사용자에게 입력을 받는다면, 문제를 일으키는 가장 간단한 입력을 주세요.

둘째로, 프로그램을 청소하세요. 쓰지 않는 코드를 지우고 가능한 한 읽기 쉽도록 프로그램을 재구성하세요. 예를 들어, 문제가 프로그램의 깊이 중첩된 부분에 있다고 의심된다면, 그 부분을 더 간단한 구조로 다시 작성하도록 해보세요. 큰 함수가 의심이 간다면, 더 작은 함수들로 나누고 각각 검사하세요.

종종 최소 검사 사례를 찾는 과정은 여러분을 버그로 인도합니다. 한가지 상황에서 프로그램이 동작하지만 다른 경우에는 그렇지 않은 것을 발견했다면, 어떤 일이 일어나는지에 대한 실마리를 줄 것입니다.

비슷하게, 코드의 일부를 다시 작성하는 것은 미묘한 버그를 찾는데 도움을 줍니다. 프로그램에 영향을 줄 수 없다고 생각하는 변경을 가했을 때, 영향이 있다면, 여러분이 비밀을 엿본 것일 수 있습니다.

의미 오류

사실, 인터프리터가 잘못에 대한 아무런 정보를 제공하지 않기 때문에, 의미 오류는 디버깅하기 가장 어렵습니다. 여러분은 단지 프로그램이 어떠해야 하는 지만 알뿐입니다.

첫 번째 단계는 프로그램 텍스트와 여러분이 보고 있는 동작을 연결하는 것입니다. 프로그램이 실제로 하고 있는 것에 대한 가설이 필요합니다. 이 것을 어렵게 하는 한가지는 컴퓨터가 아주 빨리 실행한다는 것입니다.

여러분은 종종 프로그램을 사람의 속도로 늦출 수 있기를 원할 것인데, 어떤 디버거로는 그렇게 할 수 있습니다. 그러나 잘 자리잡은 몇 개의 print 문을 넣는데 걸리는 시간은, 디버거를 설정하고, 중단점들을 넣거나 빼고, 오류가 발생한 곳까지 프로그램을 진행시키는 것보다 종종 짧습니다.

프로그램이 동작하지 않아요.

스스로에게 이런 질문들을 던져야 합니다:

  • 프로그램이 해야 할 것으로 여겨지지만 일어나지 않는 것처럼 보이는 것이 있습니까? 그 기능을 수행하는 코드 영역을 찾고 여러분이 생각하기에 실행되어야 할 시점에 실행되는지 확인하세요.
  • 일어나지 말아야 할 것이 있나요? 프로그램에서 그 기능을 수행하는 코드들 찾고 실행되지 않아야 할 때 실행되는지 보세요.
  • 여러분이 기대하지 않는 효과를 일으키는 코드 영역이 있습니까? 문제의 코드를 이해하는지 확인하세요, 다른 파이썬 모듈의 함수나 메쏘드들에 대한 호출을 수반한다면 특히 중요합니다. 여러분이 호출하는 함수들의 설명서를 읽으세요. 간단한 검사 사례를 작성해서 시험해보고 결과를 검사하세요.

프로그램을 작성하기 위해서는, 프로그램의 동작에 관한 사고 모델(mental model)이 있어야 합니다. 여러분이 기대하는 것을 하지 않는 프로그램을 작성한다면, 종종 문제는 프로그램에 있는 것이 아니라, 여러분의 사고 모델에 있습니다.

여러분의 사고 모델을 바로잡는 가장 좋은 방법은 프로그램을 구성 요소들(보통 함수와 메쏘드들)로 분해하고 각각의 구성 요소들을 따로 검사하는 것입니다. 일단 여러분의 모델과 실제 사이의 불일치를 찾아내면, 문제를 풀 수 있습니다.

물론, 여러분이 프로그램을 개발하면서 구성 요소들을 만들고 검사하고 있어야만 합니다. 문제에 부딪히면, 약간의 새 코드들만이 올바름이 확인되지 않은 상태여야 합니다.

크고 고약하게 꼬인 표현식이 있는데 기대한대로 동작하지 않아요.

복잡한 표현식을 쓰는 것은 가독성이 있는 한 상관없습니다만, 디버깅하기는 어려울 수 있습니다. 종종 복잡한 표현식을 여러 개의 임시 변수들로의 대입으로 분해하는 것이 좋습니다.

예를 들어:

self.hands[i].addCard(self.hands[self.findNeighbor(i)].popCard())

이 것을 이렇게 다시 쓸 수 있습니다:

neighbor = self.findNeighbor(i)
pickedCard = self.hands[neighbor].popCard()
self.hands[i].addCard(pickedCard)

구체적인 버전은 변수 이름이 추가적인 문서화를 제공하기 때문에 더 읽기 쉽고, 중간 변수들의 형을 검사하고 값들을 표시할 수 있기 때문에 더 디버깅하기 쉽습니다.

커다란 표현식에서 일어날 수 있는 또 다른 문제는 계산의 순서가 여러분이 기대하는 것과 다를 수 있다는 것입니다. 예를 들어, 여러분이 표현식 \(\frac{x}{2 \pi}\) 를 파이썬으로 옮기고 있다면, 이렇게 쓸 수 있습니다:

y = x / 2 * math.pi

이 것은 올바르지 않은데, 곱셈과 나눗셈이 같은 우선순위를 갖고 왼쪽에서 오른쪽으로 계산되기 때문입니다. 그래서 이 표현식은 \(x \pi / 2\) 를 계산합니다.

표현식을 디버깅하는 좋은 방법은 계산 순서를 구체적으로 만들기 위해 괄호를 넣는 것입니다:

y = x / (2 * math.pi)

계산의 순서를 확신하지 못할 때마다, 괄호를 사용하세요. (여러분이 의도한 바를 한다는 측면에서) 프로그램이 올바르게 될 뿐만 아니라, 우선 순위 규칙을 암기하고 있지 않는 다른 사람들이 더 읽기 쉬워집니다.

함수나 메쏘드가 기대하는 것을 돌려주지 않아요.

복잡한 표현식의 return 문이 있다면, 반환 전에 반환 값을 인쇄할 기회가 있습니다. 다시 한번, 임시 변수를 사용할 수 있습니다. 예를 들어, 이렇게 하는 대신:

return self.hands[i].removeMatches()

이렇게 쓸 수 있습니다:

count = self.hands[i].removeMatches()
return count

이제 여러분은 반환하기 전에 count 의 값을 표시할 기회가 있습니다.

진짜, 진짜 막혔어요, 도움이 필요합니다.

먼저, 몇 분 간 컴퓨터로부터 떨어지도록 하세요. 컴퓨터는 뇌에 영향을 줘서 이런 증상을 유발하는 웨이브를 발산합니다:

  • 좌절과 분노.
  • 미신적인 믿음들 (“컴퓨터가 나를 미워해”) 과 마술적 사고 (“프로그램은 내가 모자를 거꾸로 쓸 때만 동작해”).
  • 무작위 프로그래밍 (모든 가능한 프로그램을 만든 다음 올바른 것을 하는 것을 고름으로써 프로그램 하려는 시도).

만약 여러분이 이 중 어느 한가지 증상이라도 보인다면, 일어나서 산책을 나가세요. 안정이 될 때, 프로그램에 대해 생각하세요. 그것이 뭘 하지요? 그 동작의 가능한 원인들로는 뭐가 있을까요? 마지막으로 프로그램이 동작했던 때는 언제였고, 그 다음에 한일이 뭐였나요?

때로 버그를 발견하려면 단지 시간이 걸릴 뿐입니다. 저는 종종 컴퓨터에서 떨어져서 제 마음을 풀어놓을 때 버그를 찾습니다. 버그를 찾기에 가장 좋은 몇 가지 장소들은 기차와 샤워장과 잠들기 직전의 침대입니다.

아니요, 저는 진짜로 도움이 필요해요.

그럴 수 있습니다. 최고의 프로그래머들 조차도 가끔은 막힙니다. 때로 여러분이 한 프로그램에서 너무 오래 일했기 때문에 오류를 보지 못하는 겁니다. 단지 필요한 것은 새로운 눈입니다.

다른 사람을 개입시키기 전에, 여러분이 준비되었는지 확인하세요. 프로그램은 가능한 한 간단해야 하고, 오류를 일으키는 가장 간단한 입력으로 일하고 있어야 합니다. 적절한 장소들에 print 문들이 있어야 합니다(그리고 그들이 만드는 출력은 이해하기 용이해야 합니다). 여러분은 문제를 충분히 잘 이해해서 간결하게 설명할 수 있어야 합니다.

다른 사람을 데려와 도움을 요청할 때, 그들이 필요로 하는 정보를 제공해야 합니다:

  • 오류 메시지가 있다면, 그 것이 무엇이고 프로그램의 어떤 부분을 가리키나요?
  • 오류가 발생하기 전에 여러분이 마지막으로 한일은 무엇인가요? 여러분이 작성한 코드의 마지막 줄은 무엇인가요? 실패한 새 검사 사례는 무엇인가요?
  • 지금까지 여러분이 시도한 것은 무엇이고, 무엇을 배웠나요?

버그를 발견하면, 잠시 시간을 들여 더 빨리 찾기 위해 할 수 있었던 것에 대해 생각해보세요. 다음에 비슷한 것을 볼 때, 버그를 더 빨리 찾게 될 것입니다.

기억하세요, 목표는 단지 프로그램이 동작하게 만드는 것이 아닙니다. 목표는 프로그램이 동작하도록 만드는 법을 배우는 것입니다.