티스토리 뷰

반응형

단어 수 세기

어떤 주어진 문자열에서 단어를 세어, 단어들이 자주 나오는 순으로 정렬하는 방법에 대해서 알아보자.

종이와 펜으로 센다면

만약 어떤 책의 한 페이지에 나온 단어의 빈도수를 조사하기 위해서 종이와 펜이 주어져 있다면 어떻게 풀것인가? 보통 다음과 같은 방법을 생각해 낼 수 있을 것이다.

  1. 종이를 세로로 반으로 접는다.
  2. 왼쪽에는 단어를 쓰고, 오른쪽에는 단어가 나타난 횟수만큼 빗금을 친다. (나이가 좀 있으신 분들은 바를 정자를 쓰면 됩니다.)
  3. 책을 읽어나가면서 각 단어를 읽을 때 마다 이미 표기된 단어에는 빗금을 추가하고, 새로 만난 단어는 왼쪽에 단어를 추가하고 오른쪽에 빗금 하나를 친다.

원시적인 방법이긴 하지만, 이 방법이 사실상의 정도(正道)이며, 우리가 작성해야하는 코드 역시 무슨 마법 같은 방법을 쓰는것이 아니라 이와 똑같은 방법을 사용해야 한다.

종이와 펜으로 단어를 세는 시스템(?)에서는 단어의 출현 횟수를 기록해두는 메모 혹은 단어별 출현빈도의 맵(map)이 있었다. 우리는 어떻게 이걸 프로그램 내에서 사용할 수 있을까?

사전 - 파이썬의 맵/해시테이블

종이와 펜을 이용한 단어 세기에서 사용했던 테이블은 기존의 리스트(배열)과는 조금 다른 측면을 가지고 있는데, 일단 어떤 단어와 그 빈도수가 하나의 짝을 구성하여 모여있는 집합이라는 점이다. 그리고 0, 1, 2, 3..과 같은 인덱스 기반으로 값을 찾는 것이 아니라, 단어 자체를 기반으로 값을 찾고, 그 단어의 짝인 '노출 빈도'를 액세스하여 계속 업데이트 해 나가는 식으로 데이터를 사용했다.

그리고 또 한가지 중요한 것은 특정한 단어가 한 번 더 나왔을 때 그 출현 빈도를 업데이트하기 위해서는 해당 단어-빈도의 짝을 매우 빠르게 찾을 수 있어야 한다는 점이다.

이러한 요건들을 만족하기 위해서는 특별한 방법으로 데이터를 저장하는 자료 구조가 필요한데, 여기에 적합한 자료 구조가 맵 또는 해시 테이블이라고 하는 구조이며, 파이썬에서는 사전(dictionary)이라는 이름으로 기본적으로 이런 구조를 제공하고 있다.

키-값 쌍의 집합인 사전

파이썬의 사전(dictionary, 줄여서 dict)은 키-값이 짝을 이루는 형태로 개별 데이터를 구성하고, 키를 기반으로 저장된 데이터를 찾는다. 키가 될 수 있는 값은 "고유성"을 가져야 하는데 (키가 중복되는 두 개의 값을 구분할 수 없으므로) 정수나 문자열과 같이 immutable한 기본 파이썬 데이터 타입은 모두 사전의 키가 될 수 있다.1

문자열 그 자체의 값은 사전의 키가 될 수 있으므로, 단어를 세는데 있어서 '단어(문자열) - 횟수(정수)'의 짝은 사전의 원소로 사용가능하다. 그렇다면 이 사전 타입을 써서 단어의 빈도를 세는 과정을 구성해보자.

  1. 빈 사전을 만든다.
  2. 특정 단어가 사전에 포함되어 있는지 확인한다.
  3. 만약 포함되어 있다면 해당 단어를 키로 하는 값을 +1 하여 업데이트한다.
  4. 포함되어 있지 않다면 값을 1로 하는 원소를 추가한다.
  5. 모든 단어에 대해서 2~4의 과정을 반복하면 단어별 출현 빈도를 셀 수 있다.

빈 사전을 만드는 방법

사전은 dict 타입의 인스턴스이며, dict()를 인자 없이 호출하여 빈 사전을 만들 수 있다. 사전을 만드는 또 다른 방법은 {키: 값, 키: 값....} 의 리터럴이며, { }이라고만 써서 빈 사전을 표현할 수도 있지만 빈 중괄호 리터럴은 비어있는 set 으로 혼동할 우려가 있으므로 쓰지 않는 편을 권한다.

그외의 사전을 만드는 방법으로는 zip 객체를 이용해서 데이터를 파퓰레이팅하는 방법이 있다. 그러니까 사전의 키가 되는 값들을 가지고 있는 연속열과, 그 키에 해당하는 값을 가지고 있는 (순서가 매칭되는) 연속열을 zip() 함수로 묶어서 dict()에 인자로 넘길 수 있다.

values = [1, 2, 3, 4, 5]
keys = "abcde"
aDict = dict(zip(keys, values))

사전의 원소를 액세스하기

사전의 특정한 원소를 액세스하는 방법은 리스트의 원소를 액세스하는 방법과 비슷하다. 대괄호를 사용하여 사전이름[키]와 같은 식으로 사전의 특정한 원소를 액세스할 수 있다.

aDict['a']  ## 기존 원소를 액세스
# 1
aDict['a'] = 9 # 기존 원소를 업데이트
# aDict = {'a': 9, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
aDict['f'] = 99  ## 새 원소를 추가
# aDict = {'a': 9, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 99}

## 존재하지 않는 키로 액세스하는 경우, KeyError가 발생한다.
In [5]: d['h']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-5-23d99a0548be> in <module>()
----> 1 aDict['h']

KeyError: 'h'

사전에 특정 키가 있는지 확인하는 방법

파이썬에서 특정한 집합 컨테이너 내에 특정 원소가 있는지를 판별할 때, in 연산자를 쓰는데, 사전 역시 특정 키를 in 연산자와 결합하여 해당 키가 있는지 알 수 있다.

만약 특정한 키가 아니라 값이 사전 내에 있는지를 보기 위해서는 사전 자체의 API를 이용할 수 없다. 대신에 사전은 keys(), values()라는 두 개의 메소드를 제공하고 있고, 이 메소드를 이용해서 키와 값의 각각의 리스트를 얻을 수 있다. (단 이 때 순서는 불명이다.) 따라서 values()를 통해서 얻은 리스트에 특정한 값이 있는지를 체크할 수 있다.

사전을 순회하기

파이썬에서 사전은 기본적으로 키의 연속열처럼 취급된다. 따라서 for ... in ... 문을 통해서 사전의 키를 순회할 수 있다.

for key in aDict:
    print(key, ":", aDict[key])

튜플이나 리스트가 emuerated() 함수에 의해 인덱스와 원소값의 쌍을 순회할 수 있는 것 처럼, 사전은 items() 메소드를 통해서 (key, value) 의 쌍을 순회할 수 있다. 따라서 위의 코드는 아래와 동치이다.


for key, value in aDict.items():
    print(key, ':', value)

사전을 정렬하기

단어의 빈도를 센 다음에 빈도순으로 정렬하는 것까지 다뤄야 한다. 그런데 사전은 키 기반으로 값을 빠르게 찾을 수 있는 구조이며, 키-값 쌍의 어떠한 명시적인 순서를 부여하지는 않는다. 따라서 사전의 각 원소를 (키, 값)의 쌍이 연결된 각각의 튜플로 보고, 튜플의 리스트를 정렬하는 방식으로 원하는 결과를 얻을 수 있다. 이 때 앞에서 말한 items()를 쓸 수 있다.

연속열의 정렬된 사본을 만드는 sorted() 함수는 정렬 기준을 만드는 키 함수를 인자로 전달할 수 있는데, 각 원소가 (key, value)로 구성되어 있다면 두 번째 요소를 기준으로 정렬하려 할 때의 키 함수의 형식은 lambda x: x[1]이 된다.

풀이

최종적으로 풀이는 다음과 같다. 최소한 주어진 입력의 데이터는 세 단어 이상인 것으로 본다.

def words_sorted_by_occurrence(text):
    """
    주어진 문자열 데이터에 대해서 단어별로 빈도수를 세고
    자주 등장한 순서대로 정렬하여 (단어, 빈도수)의 튜플의
    리스트를 리턴해준다. 

    * 참고로 빈도수가 같은 경우에는 단어의 사전수순으로 정렬한다.
    """

    d = dict()
    
    words = text.split() ## 주어진 문자열을 단어별로 분리

    ## 각 단어에 대해서 빈도수를 센다.
    for word in words:
        if word in d:
            d[word] = d[word] + 1
        else:
            d[word] = 1

    result = sorted(d.items(), key=(lambda x: x[1], lambda x: x))
    return result

if __name__ == '__main__':
    source = input('텍스트를 입력해주세요(세 단어 이상)')
    data = words_sorted_by_occurrence(source)
    
    print('가장 많이 등장한 단어는 다음과 같습니다.')
    for (word, occ) in data[:3]:
        print(word, '(빈도:', occ, ')')

도전 과제

위의 코드에서 주어진 입력이 3단어 미만인 경우에 대한 예외처리를 해보자. 그냥 3개 미만까지의 결과를 출력할 수도 있고, 에러 메시지를 출력하고 다시 시도하게 할 수도 있을 것이다.


1 튜플 역시 immutable(변경 불가능)하는 점에서 사전의 키가 될 수 있다. 또한 내부적으로 해시를 만들 수 있는 커스텀 클래스의 인스턴스 역시 사전의 키가 될 수 있다. 하지만 리스트나 사전과 같이 변경 가능한 객체는 사전의 키가 될 수 없다.


반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함