사례 연구: 자료 구조 선택

단어 빈도 분석

[analysis]

평소처럼, 제 답을 보기 전에 다음 문제들을 최소한 시도해 보기는 해야 합니다.

[연습 13.1.]

파일을 읽어서, 각 행을 단어들로 분해하고, 공백문자와 구두점들을 제거한 후, 소문자로 변환하는 프로그램을 작성하세요.

힌트: string 모듈은 몇 개의 문자열들을 제공하는데, whitespace 는 스페이스(space), 탭(tab), 개행(newline) 문자 등을 포함하고, 라는 문자열을 제공하는데, punctuation 은 구두점 문자들을 포함합니다. 파이썬이 상스러운 말을 뱉도록 할 수 있는지 봅시다:

>>> import string
>>> print string.punctuation
!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~

또한, 문자열 메쏘드 strip, replace, translate의 사용도 고려해 볼만 합니다.

[연습 13.2.]

구텐버그 프로젝트(gutenberg.org)로 가서, 저작권이 만료되어 단순 텍스트 형식으로 제공되는 책들 중에서 사장 좋아하는 것을 다운로드 받으세요.

다운로드 한 책을 읽어서, 처음에 나오는 머리글들을 건너뛴 후, 나머지 부분을 앞선 연습과 같이 처리하도록, 앞에서 만든 프로그램을 수정하세요.

그런 다음 책에 나오는 총 단어 수와 각 단어가 사용된 횟수를 세도록 프로그램을 수정하세요.

책에 사용된 서로 다른 단어의 수를 인쇄하세요. 다른 저자에 의해 다른 시기에 씌어진 다른 책들을 비교해보세요. 어떤 저자가 가장 많은 어휘를 사용하나요?

[연습 13.3.]

책에서 가장 많이 사용된 20개의 단어를 인쇄하도록 앞의 연습에서 만든 프로그램을 수정하세요.

[연습 13.4.]

단어 목록([wordlist] 절을 보세요)을 읽어서 책의 단어들 중 이 목록에 없는 단어들을 모두 인쇄하도록 앞의 프로그램을 수정하세요. 이 중 얼마나 철자 오류인가요? 이 중 얼마나 단어 목록에 들어가야만 하는 일상어고, 진짜 모호한 것은 얼마나 됩니까?

난수

같은 입력이 주어질 때, 대부분의 컴퓨터 프로그램은 항상 같은 출력을 만들기 때문에 결정론적이라고 말합니다. 같은 계산이 같은 결과를 줄 것이라고 기대할 수 있기 때문에, 결정론은 보통 좋은 것입니다. 하지만, 어떤 응용에서는, 컴퓨터가 예측 불가능하길 원합니다. 게임이 자명한 예 중 하나지만 이 밖에도 더 있습니다.

프로그램을 진정한 의미에서 비결정론적으로 만드는 것은 그리 쉬운 일이 아닙니다만, 적어도 비결정론적으로 보이게끔 만드는 방법들은 있습니다. 한가지 방법은 의사 난수(pseudorandom)를 만드는 알고리즘을 사용하는 것입니다. 의사 난수는 결정론적인 계산에 의해 만들어지기 때문에 진짜 난수가 아닙니다만, 단지 숫자들을 들여다보는 것 만으로는 난수와 구별하는 게 거의 불가능합니다.

random 모듈은 의사 난수(앞으로는 그냥 “난수”라고 부르겠습니다)를 만드는 함수들을 제공합니다.

random 함수는 0.0 과 1.0 사이의 (0.0은 포함하지만 1.0은 포함하지 않습니다) 실수 난수를 돌려줍니다. random 을 호출할 때마다, 긴 수열의 다음 숫자를 얻게 됩니다. 예를 보려면, 이 순환을 실행하세요:

import random

for i in range(10):
    x = random.random()
    print x

randint 함수는 매개변수 low 와 high를 받아서 low 와 high 사이(둘 모두 포함합니다)의 정수를 돌려줍니다.

>>> random.randint(5, 10)
5
>>> random.randint(5, 10)
9

시퀀스의 요소를 임의로 선택하려면, choice를 사용할 수 있습니다:

>>> t = [1, 2, 3]
>>> random.choice(t)
2
>>> random.choice(t)
3

random 모듈은 정규(Gaussian), 지수(exponential), 감마(gamma)를 포함한 몇 가지 연속 분포를 따르는 난수들을 만드는 함수도 제공합니다.

[연습 13.5.]

[histogram] 절에서 정의된 히스토그램을 받아들여서, 히스토그램으로부터 빈도에 비례하는 확률로 임의의 값을 선택해서 돌려주는 함수 choose_from_hist 를 작성하세요. 예를 들어, 이런 히스토그램에 대해서는:

>>> t = ['a', 'a', 'b']
>>> hist = histogram(t)
>>> print hist
{'a': 2, 'b': 1}

여러분의 함수는 \(2/3\) 의 확률로 ’a’를, \(1/3\) 의 확률로 ’b’를 돌려줘야 합니다.

단어 히스토그램

계속 가기 전에 앞의 연습을 시도해야만 합니다. 제 해답을 http://thinkpython.com/code/analyze_book.py에서 다운로드 할 수 있습니다. http://thinkpython.com/code/emma.txt 도 필요할겁니다.

여기에 파일을 읽어서 들어있는 단어들의 히스토그램을 만드는 프로그램이 있습니다:

import string

def process_file(filename):
    hist = dict()
    fp = open(filename)
    for line in fp:
        process_line(line, hist)
    return hist

def process_line(line, hist):
    line = line.replace('-', ' ')

    for word in line.split():
        word = word.strip(string.punctuation + string.whitespace)
        word = word.lower()

        hist[word] = hist.get(word, 0) + 1

hist = process_file('emma.txt')

이 프로그램은 제인 오스틴(Jane Austen)작 엠마(Emma)의 글이 들어있는 emma.txt를 읽어 들입니다.

process_file 은 파일의 행을 순환하면서 process_line로 한번에 한 줄씩 넘겨줍니다. 히스토그램 hist 는 누산기로 사용됩니다.

process_line 문자열 메쏘드 replace 로 하이픈을 스페이스로 바꾼 후에 split 로 행을 문자열의 리스트로 분해합니다. 단어들의 리스트들 탐색하면서 구두점들을 제거하고 소문자로 바꾸기 위해 strip 과 lower를 사용합니다. (문자열을 바꾼다는 표현은 줄임 말입니다. 문자열이 수정 불가능하다는 것을 기억하세요. strip 과 lower 와 같은 메쏘드들은 새 문자열을 돌려줍니다.)

마지막으로, process_line은 새 항목을 만들거나 기존의 것을 증가시킴으로써 히스토그램을 갱신합니다.

파일에 있는 모든 단어의 수를 세려면, 히스토그램의 빈도를 더하면 됩니다:

def total_words(hist):
    return sum(hist.values())

서로 다른 단어들의 개수는 딕셔너리에 있는 항목의 개수입니다:

def different_words(hist):
    return len(hist)

여기 결과를 인쇄하는 약간의 코드가 있습니다:

print 'Total number of words:', total_words(hist)
print 'Number of different words:', different_words(hist)

그리고 결과는:

Total number of words: 161080
Number of different words: 7214

가장 흔한 단어

가장 흔한 단어를 찾으려면, DSU 패턴을 적용할 수 있습니다; most_common 은 히스토그램을 받아들여서 빈도의 역순으로 정렬된 단어-빈도 튜플의 리스트를 돌려줍니다:

def most_common(hist):
    t = []
    for key, value in hist.items():
        t.append((value, key))

    t.sort(reverse=True)
    return t

여기에 가장 흔한 10개의 단어들을 인쇄하는 순환이 있습니다:

t = most_common(hist)
print 'The most common words are:'
for freq, word in t[0:10]:
    print word, '\t', freq

그리고 이 것은 엠마(Emma) 의 결과입니다:

The most common words are:
to  5242
the   5205
and   4897
of  4295
i   3191
a   3130
it  2529
her   2483
was   2400
she   2364

선택 매개변수

우리는 가변적인 개수의 인자를 받아들이는 내장함수와 메쏘드들을 본적이 있습니다. 선택적 인자를 받아들이는 사용자 정의 함수 또한 쓸 수 있습니다. 예를 들어, 여기에 히스토그램에서 가장 흔한 단어들을 인쇄하는 함수가 있습니다.

def print_most_common(hist, num=10):
    t = most_common(hist)
    print 'The most common words are:'
    for freq, word in t[:num]:
        print word, '\t', freq

첫 번째 매개변수는 필수고; 두 번째는 선택입니다. num 의 내정 값(default value)은 10입니다.

여러분이 오직 하나의 인자만 제공한다면:

print_most_common(hist)

num은 내정 값을 받습니다. 여러분이 두 개의 인자를 제공하면:

print_most_common(hist, 20)

num 은 대신 인자의 값을 받게 됩니다. 다른 말로 하면, 선택 인자는 내정 값을 번복(override)합니다.

만약 함수가 필수와 선택 매개변수를 모두 갖고 있다면, 모든 필수 매개변수들이 먼저 와야 하고, 그 뒤로 선택 매개변수가 나와야 합니다.

딕셔너리 뺄셈

책에 나오는 단어들 중 words.txt 의 단어 목록에 없는 것들을 찾는 것은 여러분이 차집합이라고 알고 있는 문제입니다; 즉, 우리는 한 집합 (책에 있는 단어들)의 단어들 중 다른 집합 (목록에 있는 단어들)에 속하지 않는 것들을 찾길 원합니다.

subtract 는 딕셔너리 d1 과 d2를 받아서, d1 의 모든 키 중에서 d2에 없는 것들을 포함하는 새 딕셔너리를 돌려줍니다. 우리는 값에 대해서는 별 관심이 없기 때문에, 모두 None 으로 설정합니다.

def subtract(d1, d2):
    res = dict()
    for key in d1:
        if key not in d2:
            res[key] = None
    return res

책에 나오는 단어들 중 words.txt에 없는 것들을 찾으려면, process_file 를 써서 words.txt의 히스토그램을 만든 후에, 빼줍니다:

words = process_file('words.txt')
diff = subtract(hist, words)

print "The words in the book that aren't in the word list are:"
for word in diff.keys():
    print word,

여기에 Emma의 결과 일부가 있습니다: Here are some of the results from Emma:

The words in the book that aren't in the word list are:
 rencontre jane's blanche woodhouses disingenuousness
friend's venice apartment ...

단어들의 일부는 이름이나 소유격입니다. “rencontre” 같은 것들은 더 이상 일상적으로 사용되지 않습니다. 하지만 몇 가지는 목록에 진짜로 들어가야만 하는 일상어입니다!

[연습 13.6.]

파이썬은 set 이라는 자료 구조를 제공하는데, 자주 쓰이는 집합 연산들을 많이 제공합니다. http://docs.python.org/2/library/stdtypes.html#types-set 의 설명서를 읽고 책에 있는 단어들 중 단어 목록에 없는 것들을 찾기 위해 집합 뺄셈을 사용하는 프로그램을 작성하세요. 답: http://thinkpython.com/code/analyze_book2.py.

임의의 단어들

[randomwords]

히스토그램에서 임의의 단어를 고르려고 할 때, 가장 간단한 알고리즘은, 관찰된 빈도에 따라 각 단어들의 사본들로 리스트를 만들고, 그 리스트에서 선택하는 것입니다:

def random_word(h):
    t = []
    for word, freq in h.items():
        t.extend([word] * freq)

    return random.choice(t)

* freq 표현은 문자열 word 의 사본 freq 개로 구성된 리스트를 만듭니다. extend 메쏘드는 인자가 시퀀스라는 점을 제외하고는 append와 유사합니다.

[연습 13.7.] [randhist]

이 알고리즘은 동작합니다만, 별로 효율적이지는 않습니다; 임의의 단어를 고를 때마다, 원래 책만큼 큰 리스트를 새로 만듭니다. 명백한 개선은 리스트를 한번 만든 후에, 여러 번 선택하는 것입니다만, 리스트가 큰 것은 여전합니다.

대안은 이렇습니다:

  1. keys를 사용해서 책에 있는 단어들의 리스트를 얻습니다.
  2. 단어 빈도의 누적합(연습 [cumulative]을 보세요)이 있는 리스트를 만듭니다. 이 리스트의 마지막 항목은 책에 있는 단어의 총 개수, \(n\) , 입니다.
  3. 1 에서 \(n\) 사이에서 난수를 고르세요. 이분 검색(연습 [bisection]을 보세요)을 사용해서 누적합의 난수가 삽입될 지수를 찾으세요.
  4. 지수를 사용해서 단어 리스트에서 해당 단어를 찾으세요.

이 알고리즘을 사용해서 책에서 임의의 단어를 선택하는 프로그램을 작성하세요. 답: http://thinkpython.com/code/analyze_book3.py.

마르코프 분석

[markov]

책에서 단어를 임의로 선택한다면, 어휘에 대한 감은 얻을 수 있겠지만, 아마도 구문을 얻지는 못할 것입니다:

this the small regard harriet which knightley's it most things

연속되는 단어들 간에 아무런 관계가 없기 때문에, 임의의 단어 연속은 거의 의미를 만들지 못합니다. 예를 들어, 실제 구문에서 여러분은 “the” 와 같은 관사 뒤에는 형용사나 명사가 오지, 동사나 부사가 나오지는 않을 거라고 기대할 것입니다.

이런 종류의 관계를 측정하는 한가지 방법이 마르코프 분석(Markov Analysis)인데, 주어진 단어들의 시퀀스에 대해서, 다음에 오는 단어들의 확률 특성을 기술합니다. 예를 들어, 노래 Eric, the Half a Bee 는 이렇게 시작합니다:

Half a bee, philosophically,
Must, ipso facto, half not be.
But half the bee has got to be
Vis a vis, its entity. D’you see?
But can a bee be said to be
Or not to be an entire bee
When half the bee is not a bee
Due to some ancient injury?

이 글에서, “half the” 구 다음에는 항상 단어 “bee” 가 오는 반면, “the bee” 구의 뒤에는 “has” 나 “is”가 옵니다.

마르코프 분석의 결과는 (“half the” 나 “the bee”와 같은) 접두어에서 모든 가능한 (“has” 와 “is” 같은) 접미어로의 대응입니다.

이 대응이 주어졌을 때, 아무 접두어로나 시작해서 가능한 접미어들에서 임의로 선택하는 방법으로 임의의 텍스트를 만들 수 있습니다. 그 다음에는, 접두어의 끝과 새 접미어를 연결해서 새 접미어를 만드는 식으로 반복합니다.

예를 들어, 접두어 “Half a” 로 시작하면, 글에서 오직 한번만 등장하는 접두어이기 때문에 다음 단어는 “bee” 가 되어야만 합니다. 다음 접두어는 “a bee” 가 되어, 다음 접미어는 “philosophically”, “be”, “due” 중 하나가 됩니다.

이 예에서 접두어의 길이는 항상 2이지만, 어떤 접두어 길이로도 마르코프 분석을 할 수 있습니다. 접두어의 길이를 분석의 “차수(order)” 라고 부릅니다.

[연습 13.8.]

마르코프 분석:

  1. 파일에서 글을 읽어서 마르코프 분석을 수행하는 프로그램을 작성하세요. 결과는 접두어를 가능한 접미어들의 컬렉션으로 대응시키는 딕셔너리가 되어야 합니다. 컬렉션은 리스트, 튜플, 딕셔너리 중 어느 것이건 가능합니다; 적절한 선택은 여러분의 몫입니다. 접두어 길이 2로 여러분의 프로그램을 검사할 수 있습니다만, 다른 길이로 쉽게 바꿀 수 있도록 하는 방식으로 프로그램을 작성해야만 합니다.

  2. 앞의 프로그램에 마르코프 분석에 기반해서 임의의 텍스트를 만드는 함수를 추가하세요. 여기에 접두어 길이 2를 사용한 Emma의 예가 있습니다:

    He was very clever, be it sweetness or be angry, ashamed or only amused, at such a stroke. She had never thought of Hannah till you were never meant for me?" "I cannot make speeches, Emma:" he soon cut it all himself.

    이 예를 위해서, 저는 구두점이 단어에 붙은 채로 남겨두었습니다. 결과는 거의 문법적으로 옮지만, 완벽하지는 않습니다. 의미적으로는, 거의 말이 되는 듯하지만, 완벽하지는 않습니다.

    접두어 길이를 늘리면 어떻게 될까요? 임의의 텍스트는 더 그럴듯해지나요?

  3. 일단 여러분의 프로그램이 동작하면, 매쉬업(mash-up)을 시도해보세요: 둘이나 그 이상의 책의 글을 분석하면, 여러분이 만드는 임의의 텍스트는 흥미로운 방식으로 그 소스들의 단어나 구절들을 뒤섞게 됩니다.

출처: 이 사례 연구는 Kernighan and Pike, The Practice of Programming, Addison-Wesley, 1999 의 예에 기반합니다.

여러분은 더 진행하기 전에 이 연습을 시도해봐야 합니다; 그런 다음 제 해답을 http://thinkpython.com/code/markov.py에서 다운로드 할 수 있습니다. http://thinkpython.com/code/emma.txt 도 필요할겁니다.

자료 구조

마르코프 분석을 사용해서 임의의 텍스트를 만드는 것은 재미있기도 하지만, 이 연습에는 요점이 있습니다: 자료 구조 선택. 앞의 연습에 대한 답에서, 여러분은 선택했어야 합니다:

  • 어떻게 접두어를 나타낼 것인가.
  • 어떻게 가능한 접미어들의 컬렉션을 나타낼 것인가.
  • 어떻게 각 접두어에서 가능한 접미어의 컬렉션으로의 대응을 나타낼 것인가.

예, 마지막 것은 쉽습니다; 우리가 만난 형들 중 대응을 표현할 수 있는 것은 딕셔너리가 유일합니다, 그러니 자연스러운 선택입니다.

접두어로는, 가장 자명한 선택지는 문자열, 문자열의 리스트, 문자열의 튜플입니다. 접미어로는, 한가지 선택은 리스트고, 다른 하나는 히스토그램 (딕셔너리)입니다.

어떻게 선택해야 할까요? 첫 번째 단계는 각 자료 구조마다 여러분이 구현할 필요가 있는 연산들에 대해 생각하는 것입니다. 예를 들어, 만약 현재의 접두어가 “Half a” 이고 다음 단어가 “bee”라면, 여러분은 다음 접두어 “a bee” 를 만들 수 있어야 합니다.

여러분의 첫 번째 선택은 요소를 더하거나 지우기가 쉽기 때문에 리스트가 될 수 있습니다만, 접두어를 딕셔너리의 키로 사용할 수 있어야 하기 때문에, 리스트를 제외합니다. 튜플로는 더하거나 지울 수는 없지만, 더하기 연산자로 새 튜플을 만들 수 있습니다:

def shift(prefix, word):
    return prefix[1:] + (word,)

shift는 단어들의 튜플, prefix, 과 문자열, word,을 받아서, 튜플을 만드는데, 첫 번째를 제외한 prefix의 모든 단어들을 포함하고, 끝에 word 를 추가한 것이다.

접미어들의 컬렉션으로, 우리가 수행할 필요가 있는 연산은 새 접미어를 추가하는 것(또는 기존의 것의 빈도를 증가시키는 것)과 임의의 접미어를 선택하는 것을 포함합니다.

새 접미어를 추가하는 것은 리스트 구현이나 히스토그램에서 비슷하게 쉽습니다. 리스트에서 임의의 요소를 선택하는 것은 쉽습니다; 히스토그램에서 선택하는 것은 효율적으로 수행하기가 더 어렵습니다(연습 [randhist]를 보세요).

지금까지 우리는 대부분 구현의 용이함에 대해서만 논의해 왔습니다만, 자료 구조를 선택하는데 이외에 고려해야 할 다른 요소들도 있습니다. 때로 한 자료 구조가 다른 것들보다 빠를 것으로 기대하는 이론적인 이유가 있습니다; 예를 들어, in 연산자는, 적어도 요소의 개수가 클 때, 리스트에서 보다 딕셔너리에서 더 빠르다고 언급했습니다.

그러나 종종 어떤 구현이 더 빠를지 미리 알 지 못합니다. 한가지 선택지는 둘 다 구현한 후 어느 것이 더 빠른지 보는 것입니다. 이 접근법을 벤치마킹(benchmarking)이라고 합니다. 실용적인 대안은 가장 구현하기 쉬운 자료 구조를 선택하고, 의도한 응용을 위해 충분히 빠른지 보는 것입니다. 만약 그렇다면, 더 개선해야 할 필요가 없습니다. 그렇지 않다면, profile 모듈과 같은, 대부분의 시간이 소요되는 프로그램상의 위치를 찾아줄 수 있는 도구들이 있습니다.

고려해야 할 다른 요소는 저장 공간입니다. 예를 들어, 접미어의 컬렉션을 위해 히스토그램을 사용하는 것은 각 단어를 한번만 저장해도 되기 때문에 더 적은 공간을 필요로 할 수 있습니다. 어떤 경우에, 공간을 절약하는 것은 여러분의 프로그램을 더 빠르게 만들 수 있고, 극단적인 경우에 메모리를 모두 소진한다면 여러분의 프로그램은 동작하지 않을 수 있습니다. 그러나 많은 응용에서, 공간은 실행 시간 다음으로 따지는 이차적인 고려사항입니다.

마지막 생각: 이 토론에서, 저는 암묵적으로 우리가 분석과 생성에 한가지 자료 구조를 사용해야 한다고 가정했습니다. 그러나, 이들은 별도의 단계이기 때문에, 한가지 구조를 분석에 사용하고, 생성을 위해서는 다른 구조로 변환하는 것도 가능합니다. 만약 생성시에 절약된 시간이 변환에 사용된 시간을 넘어선다면 전체적으로는 이득일 것입니다.

디버깅

프로그램을 디버깅할 때, 그리고 특별히 어려운 버그와 씨름할 때, 시도할 만한 것이 네 가지 있습니다:

읽기 reading:
여러분의 코드를 검토하고, 소리 내어 읽고, 여러분이 말하고자 한 것을 말하고 있는지 확인하세요.
실행 running:
수정을 가해서 다른 버전을 실행하는 식으로 실험하세요. 종종 여러분이 적절한 것을 프로그램의 적절한 장소에 표시하면, 문제가 명확해집니다만, 때때로 여러분은 비계를 만드는데 시간을 들여야만 합니다.
숙고 ruminating:
생각하는데 시간을 들이세요! 어떤 종류의 오류인가: 문법, 실행, 의미? 오류 메시지나 프로그램의 출력으로부터 어떤 정보를 얻을 수 있는가? 어떤 종류의 오류가 여러분이 보고 있는 문제를 일으킬 수 있는가? 문제가 발생하기 전에 마지막으로 바꾼 것은 무엇인가?
퇴각 retreating:
어떨 때는, 뒤로 물러나서 동작하고 이해하고 있는 프로그램을 얻을 때까지 최근의 변경들을 되돌리는 것이 최선입니다. 그런 다음 다시 만들기 시작할 수 있습니다.

막 입문한 프로그래머들은 때로 이 중 한가지에 매몰되어 다른 것들을 잊곤 합니다. 각 활동은 자신만의 실패 방식을 수반합니다.

예를 들어, 코드를 읽는 것은 문제가 오탈자 오류인 경우에 도움이 되지만, 문제가 개념적인 오해인 경우는 소용이 없습니다. 만약 여러분이 프로그램이 무엇을 하는지 이해하지 못하고 있다면, 100번을 읽어도 오류를 보지 못하는데, 오류가 여러분의 머릿속에 있기 때문입니다.

실험을 수행하는 것은 도움이 될 수 있는데, 특히 작고 간단한 검사를 실행한다면 그렇습니다. 그러나 여러분의 코드에 대해 생각하거나 읽지 않고 실험을 수행한다면, 여러분은 제가 “무작위 프로그래밍(random walk programming)”이라고 부르는 패턴에 빠질 수 있는데, 프로그램이 올바로 동작할 때까지 무작위로 변경을 가하는 과정을 의미합니다. 말할 필요도 없이, 무작위 프로그래밍에는 긴 시간이 들어갈 수 있습니다.

여러분은 생각하는데 시간을 들여야 합니다. 디버깅은 실험 과학과 같습니다. 여러분은 적어도 문제가 무엇인지에 대한 적어도 하나의 가설이 있어야 합니다. 만약 둘이나 그 이상의 가능성이 있다면, 그 중 하나를 제거할 수 있는 실험을 고안하려고 시도하세요.

잠깐 후식하는 것은 생각하는데 도움이 됩니다. 대화를 나누는 것도 마찬가지 입니다. 만약 여러분이 문제를 다른 사람에게 (또는 여러분 자신에게라도) 설명하면, 때로 질문을 마치기도 전에 답을 발견할겁니다.

그러나 최고의 디버깅 기술조차도 너무 많은 오류가 있거나 고치려고 하는 코드가 너무 크고 복잡하다면 실패할 것입니다. 때로 가장 좋은 선택은 물러나서, 동작하고 이해랄 수 있는 것을 얻을 때까지 프로그램을 단순화하는 것입니다.

막 입문한 프로그래머들은 (설사 잘못된 것이라 할지라도) 한 줄의 코드를 지우는 것도 받아들일 수 없기 때문에 종종 물러나는 것에 저항합니다. 만약 마음을 편하게 해준다면, 지우기 전에 여러분의 프로그램을 다른 파일에 복사해두세요. 그런 다음 여러분은 한번에 조금씩 조각들을 옮겨 붙여올 수 있습니다.

어려운 버그를 찾는 데는 읽기, 실행, 숙고 와 때로 퇴각이 필요합니다. 여러분이 이중 한가지 활동에만 빠져있다면, 다른 것들을 시도해보세요.

용어

결정론적 deterministic:
같은 입력이 주어질 때 실행 할 때마다 같은 일을 하는 프로그램에 주어지는 표현.
의사 난수 pseudorandom:
난수처럼 보이지만 결정론적인 프로그램에 의해 만들어진 숫자들의 시퀀스에 주어지는 표현.
내정 값 default value:
인자가 제공되지 않을 때 선택적 매개변수에 주어지는 값.
번복 override:
내정 값을 인자로 바꾸는 것.
벤치마킹 benchmarking:
대안들을 구현하고 가능한 입력들의 샘플에 대해 검사함으로써 자료 구조를 선택하는 과정.

연습

[연습 13.9.]

단어의 “랭크(rank)”는 빈도로 정렬한 단어들의 리스트에서의 위치입니다: 가장 흔한 단어는 링크 1을 갖고, 두 번째로 흔한 단어는 랭크 2를 갖는 식입니다.

지프의 법칙(Zipf’s law)은 자연어에서 단어들의 랭크와 빈도간의 관계에 대해 기술합니다(http://en.wikipedia.org/wiki/Zipf's_law). 구체적으로, 랭크 \(r\) 인 단어의 빈도, \(f\),가 다음과 같다고 예측합니다:

\[f = c r^{-s}\]

\(s\)\(c\) 는 언어와 글에 종속되는 매개변수입니다. 이 방정식의 양변에 로그를 취하면, 이런 식을 얻습니다:

\[\log f = \log c - s \log r\]

그래서 log \(f\) 를 log \(r\) 에 대해 그리면, 기울기가 \(-s\) 이고 y-절편이 log \(c\) 인 직선을 얻어야만 합니다.

파일에서 글을 읽어 들여, 단어의 빈도를 세고, 빈도의 내림차순으로, 한 줄에 단어 하나씩 log \(f\) 와 log \(r\) 을 인쇄하는 프로그램을 작성하세요. 여러분이 선택한 그래프 그리는 프로그램을 사용해서 결과를 그리고 직선을 이루는지 확인하세요. \(s\) 의 값을 추정할 수 있습니까?

답: http://thinkpython.com/code/zipf.py. 그래프를 그리려면, matplotlib 를 설치해야 합니다 (http://matplotlib.sourceforge.net/를 보세요).