Skip to content

Latest commit

 

History

History
695 lines (510 loc) · 37.3 KB

language-basic-2.org

File metadata and controls

695 lines (510 loc) · 37.3 KB

Python 언어의 기초 2: 자료형

Python3에는 아래와 같은 주요 자료형(type)이 있습니다.

  • Boolean types: None, True, False
  • Numeric types: int, float
  • Sequence types: list, tuple
  • Text sequence types: str
  • Byte sequence types: bytes

상수형 자료형

우선, 아래와 같은 상수형 자료형들이 있습니다.

# -*- coding: utf-8 -*-
# boolean
True
False
10         # int
10.5       # float
int(10.5)  # to integer
float(10)  # to float
str(10.5)  # to str

list, tuple

그리고 배열 형태의 자료형으로는 listtuple 이 대표적입니다. listtuple 은 거의 비슷하게 사용할 수 있으나, list 는 처음 생성한 이후에도 원소를 추가, 제거하는 등 변경을 가할 수 있지만, tuple 은 원소에 변경을 가할 수 없습니다.

a_list = [1, 2, 3, 4, 5]    # list
b_list = (1, 2, 3, 4, 5)    # tuple
c_list = [6, 7, 8]

a_list[2] == 3              # 특정 위치의 원소 참조. 참조번호는 0번부터 시작.
a_list[-1] == 5             # 음수인 경우, 뒤에서부터 접근.
a_list[2:4] == [3,4,5]      # 참조번호 2부터 4까지를 잘라서(slice) 반환함.
a_list[2:] == [3,4,5]       # 참조번호 2부터 끝까지를 잘라서(slice) 반환함.
a_list[:2] == [1,2]         # 처음부터 참조번호 2까지를 잘라서(slice) 반환함.
a_list[2::2] == [3,5]       # 참조번호 2부터 끝까지를 자르되, 2칸씩 건너뛰면서 잘라서(slice) 반환함.
1 in a_list == True         # 1이라는 값이 리스트에 존재하는지 여부를 반환.
a_list + c_list == [1, 2, 3, 4, 5, 6, 7, 8]     # 두 리스트를 합친 새 리스트 반환.

a_list.append(6)            # 맨 뒤에 6이라는 새 원소를 추가.
a_list.insert(0, -1)        # 0번째 참조번호에 -1이라는 새 원소를 추가.
a_list.pop(0)               # 0번째 원소를 리스트에서 제거.

a_list[2] = 0               # 참조번호 2번의 값을 0으로 변경

tuple 에 대해서는 변경을 가할 수 없기 때문에, append, insert, pop 등의 조작은 할 수 없습니다.

listtuple 에는 숫자 뿐 아니라 모든 자료형을 담을 수 있으며, 원소끼리 동질의 자료형을 가지지 않아도 됩니다.

['Tom', 'Chris', 'Timothy', 'Dan', 'Esther']     # 문자열의 리스트.
['a', 1, 'c', 2, 'e', 3]                         # 여러 타입이 섞인 리스트.
[['a', 1], ['c', 2], ['e', 3]]                   # 리스트로 이루어진 리스트.

[['Tom', 'M', 15, True], 
 ['Chris', 'F', 28, True], 
 ['Timothy', 'M', 32, False]]                    # 리스트로 이루어진 리스트.

listtuplefor 구문과 함께 사용할 수 있습니다.

name_list = ('Tom', 'Chris', 'Timothy', 'Dan', 'Esther')
for name in name_list:
    print(name)
Tom
Chris
Timothy
Dan
Esther

다음과 같이 일종의 행과 열로 이루어진 반복도 가능합니다.

a_list = [['Tom', 'M', 15, True], 
          ['Chris', 'F', 28, True], 
          ['Timothy', 'M', 32, False]]

for row in a_list:
    if row[2] > 20:
        print(row)
['Chris', 'F', 28, True]
['Timothy', 'M', 32, False]

listtuple 에 대해서는, 각 원소를 풀어헤쳐서 이름 있는 변수에 할당할 수 있습니다. (destructuring, 또는 tuple unpacking 이라고 합니다.)

a_list = [['Tom', 'M', 15, True], 
          ['Chris', 'F', 28, True], 
          ['Timothy', 'M', 32, False]]

for row in a_list:
    (name, sex, age, experience) = row
    print(name)
Tom
Chris
Timothy

간혹 여러 개의 list 를 한번에 순회해야 할 때, 인덱스 번호가 필요할 때가 있습니다. 그럴 때는 아래와 같이 enumerate 함수를 사용할 수 있습니다.

a_list = [20, 14, 19, 30, 42]
b_list = ['Tom', 'Chris', 'Timothy', 'Sandy', 'Buzz']

for index, a_element in enumerate(a_list):
    print(index, a_element, b_list[index])
0 20 Tom
1 14 Chris
2 19 Timothy
3 30 Sandy
4 42 Buzz

또는 zip() 함수를 사용할 수도 있습니다. zip() 함수는 두 개 이상의 길이가 같은 리스트를 하나로 합쳐줍니다.

a_list = [20, 14, 19, 30, 42]
b_list = ['Tom', 'Chris', 'Timothy', 'Sandy', 'Buzz']
c_list = zip(a_list, b_list)
print(list(c_list))
[(20, 'Tom'), (14, 'Chris'), (19, 'Timothy'), (30, 'Sandy'), (42, 'Buzz')]

for 구문과 함께는 아래와 같이 사용할 수 있습니다.

a_list = [20, 14, 19, 30, 42]
b_list = ['Tom', 'Chris', 'Timothy', 'Sandy', 'Buzz']
for a, b in zip(a_list, b_list):
    print(a, b)
20 Tom
14 Chris
19 Timothy
30 Sandy
42 Buzz

List comprehension을 사용해서 간편하게 list를 만들 수 있습니다.

a_list = [20, 14, 19, 30, 42]
a_gt_19_list = [v for v in a_list if v >= 19]
print(a_gt_19_list)
[20, 19, 30, 42]

위의 코드는 아래 코드와 의미적으로 동일합니다.

a_list = [20, 14, 19, 30, 42]
a_gt_19_list = []

for v in a_list:
    if v >= 19:
        a_gt_19_list.append(v)
print(a_gt_19_list)
[20, 19, 30, 42]

List comprehension을 사용한 또 다른 예입니다.

a_list = [20, 14, 19, 30, 42]
a_double_list = [v * 2 for v in a_list]
print(a_double_list)
[40, 28, 38, 60, 84]

dict

이번에는 dict 에 대해 알아봅니다.

dict 는, 특정한 key에 대해 value를 기억하는 자료형입니다. 우선 dict 에 값을 저장하고 인출하는 기본적인 동작을 알아봅시다.

a_dict = dict()     # dict를 생성
a_dict = {}         # dict를 생성 (위와 동일)

a_dict['0'] = 3          # '0'이라는 공간(key)에 3이라는 값을 저장
a_dict['Tom'] = 'M'      # 'Tom'이라는 공간에 'M'이라는 값을 저장

# a_dict['Heidi']          # KeyError 발생
a_dict.get('Heidi')      # None 반환
a_dict.get('Heidi', '?') # '?' 반환

b_dict = {'0': 3, 'Tom': 'M'}   # {} 표현형으로 dict 정의
c_dict = {'1': 4, 'Mary': 'F'}
b_dict.update(c_dict)           # b dict에 c dict의 내용을 추가하여 덮어씀

a_dict == b_dict         # 값을 하나씩 넣어서 구성한 dict와, {} 표현형으로 정의한 dict는 결과가 동일

print(a_dict)
{'Tom': 'M', '0': 3}

key에 사용할 수 있는 자료형에는 제한이 없습니다만, 숫자, 문자, 문자열 등의 기본적인 자료형, 또는 그들로 이루어진 tuple 을 사용하는 것을 권장합니다.

이어서, 조금 더 복잡한 사용법을 알아봅시다.

a_dict = {'Tom': 15, 'Chris': 28, 'Timothy': 32}

'Tom' in a_dict == True           # dict에 'Tom'이라는 키가 존재하는지 확인
a_dict.setdefault('Jenny', 20)    # Jenny라는 공간(key)이 없었다면 공간을 마련하고 20을 할당
a_dict.setdefault('Tom', 20)      # Tom이라는 공간(key)이 없었다면 공간을 마련하고 20을 할당, 아니라면 무시

print(a_dict['Jenny'] == 20)
print(a_dict['Tom'] == 15)
True
True

아래와 같이 dictkey-value 를 제거할 수 있습니다.

a_dict = {'Tom': 15, 'Chris': 28, 'Timothy': 32}

a_dict.pop('Tom')       # dict에서 'Tom' 공간을 제거하면서 그 값을 반환
del a_dict['Tom']       # dict에서 'Tom' 공간을 제거

아래와 같이 dict 에 담겨있는 keyvalue, (key, value) 의 목록을 가져올 수 있습니다.

a_dict = {'Tom': 15, 'Chris': 28, 'Timothy': 32}

print(a_dict.keys())   # dict에 마련된 공간 이름(key) 목록을 리스트로 반환
print(a_dict.values()) # dict에 저장된 값의 목록을 리스트로 반환
print(a_dict.items())  # dict에 저장된 공간 이름과 값의 목록을 중첩된 리스트로 반환
dict_keys(['Timothy', 'Tom', 'Chris'])
dict_values([32, 15, 28])
dict_items([('Timothy', 32), ('Tom', 15), ('Chris', 28)])

이렇게 가져온 목록은 for 문과 함께 사용할 수 있습니다.

a_dict = {'Tom': 15, 'Chris': 28, 'Timothy': 32}
for key in a_dict.keys():
    print(key)
Chris
Timothy
Tom
a_dict = {'Tom': 15, 'Chris': 28, 'Timothy': 32}
for value in a_dict.values():
    print(value)
28
32
15
a_dict = {'Tom': 15, 'Chris': 28, 'Timothy': 32}
for item in a_dict.items():
    print(item)
('Chris', 28)
('Timothy', 32)
('Tom', 15)
a_dict = {'Tom': 15, 'Chris': 28, 'Timothy': 32}
for k, v in a_dict.items():
    print('key={}, value={}'.format(k, v))
key=Chris, value=28
key=Timothy, value=32
key=Tom, value=15

set

이번에는 set 에 대해 알아봅시다. set 은 값들을 유일하게 저장합니다. 유일한 값의 집합을 유지하고 싶을 때 유용합니다. 직접 예를 보시죠.

a_set = set()
a_set.add(1)
a_set.add(1)
a_set.add(1)
a_set.add(2)
a_set.add(3)
a_set.add(4)
print(a_set)
set([1, 2, 3, 4])

set 에도 담을 수 있는 자료형에는 제한이 없습니다만, 숫자, 문자, 문자열 등의 기본적인 자료형, 또는 그들로 이루어진 튜플을 사용하는 것을 권장합니다.

set 에서도 in 을 사용하여 원소 포함 여부를 검사할 수 있습니다. 그리고 set 자체를 for A in BB 자리에 사용하여 각 원소를 순회하면서 작업을 수행할 수 있습니다.

str (문자열)

데이터 분석을 하면서 가장 많이 사용하게 될 작업 중 하나가 문자열을 다루는 것입니다. 여기서는 문자열에 대해서 조금 더 자세히 살펴봅시다.

  • concat
  • split
  • contains(in)
  • find, rfind
  • slicing
  • startswith
  • encoding
'Hello' + ' World' == 'Hello World'                               # 두 개의 문자열을 합친 새 문자열을 반환
'Hello Python World!'.split(' ') == ['Hello', 'Python', 'World!'] # 문자열을 구분자를 기준으로 잘라 리스트로 반환
'Hello' in 'Hello World' is True                                  # 특정 문자열이 포함되어 있는지 여부를 확인
'Hello\nWorld'                                                    # 한 줄 아래로 출력

'Tom, Hello, World!'.find(',') == 3                               # 특정 문자 또는 문자열이 처음 등장하는 위치를 반환
'Tom, Hello, World!'.find(',') == 10                              # 특정 문자 또는 문자열이 처음 등장하는 위치를 반환

'Hello Python World!'[6] == 'P'                # 특정 위치의 문자를 반환 (list와 비슷)
'Hello Python World!'[6:12] == 'Python'        # 특정 범위의 문자열을 반환
'Hello Python World!'[6:] == 'Python World!'   # 특정 범위의 문자열을 반환

len('Hello') == 5                              # 문자열의 길이를 반환
'Hello'.startswith('He') is True               # 문자열이 특정 문자열로 시작하는지 여부를 반환
'Hello'.endswith('lo') is True                 # 문자열이 특정 문자열로 끝나는지 여부를 반환
'   Hello World    '.strip() == 'Hello World'  # 문자열의 앞뒤에 있는 공백 및 개행을 제거하여 반환
'Hello World'.lower() == 'hello world'         # 문자열을 소문자로 변환하여 반환
'Hello World'.upper() == 'HELLO WORLD'         # 문자열을 대문자로 변환하여 반환

# 템플릿을 바탕으로 문자열을 생성하여 반환
'Hey {}, Welcome to {} World!'.format('Tom', 'Python') == 'Hey Tom, Welcome to Python World!'

# 리스트에 담긴 문자열들 구분자를 사용하여 하나의 문자열로 결합하여 반환
' '.join(['Welcome', 'to', 'Python', 'World']) == 'Welcome to Python World'

'안녕하세요'.encode('utf8') == b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'
b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'.decode('utf8') == '안녕하세요'

Python에서는 문자열을 표현할 때 ~”~ 와 ~”“~ 두 개를 모두 사용합니다. 그리고 여러 줄의 문자열을 쉽게 표현하기 위해서 ~”’ ”’~ 처럼 세 개의 따옴표를 연속하여 문자열을 열고 닫을 수 있습니다.

'Hello World' == "Hello World"
"It's useful to write an apostrophe"
'<a href="http://abcd.com">'

'''I am a multiline string
Hello World'''

문자열에는 표시되는 글자 외에도 특수한 기호를 표현하기 위해 약속된 규칙이 있습니다.

'\n'                            # 개행 문자
'\t'                            # 탭 문자
'\''                            # '' 내에서 ' 문자 자체를 표현하기 위함
"\""                            # "" 내에서 " 문자 자체를 표현하기 위함
'\x61'                          # hex 코드
'\\'                            # \ 문자 자체

인코딩과 한글

이번에는 인코딩에 대해서 알아보겠습니다. 한글을 다루게 되면 꼭 한 번은 이해하고 넘어가야 할 내용입니다.

우선 인코딩이란 무엇일까요?

https://upload.wikimedia.org/wikipedia/commons/thumb/b/b5/International_Morse_Code.svg/465px-International_Morse_Code.svg.png

모르스 부호는 ‘짧다’와 ‘길다’, 즉, 0과 1을 사용해서 문자의 조합을 숫자의 조합으로 표현합니다.

모르스 부호의 경우, A01 로 표현하는 것을 코드화, 즉 encode 라고 합니다. 반대로 모르스 부호로 되어있는 신호를 우리가 읽을 수 있는 문자로 복호화하는 것을 decode 라고 합니다. 그리고 국제적으로 공통된 ‘모르스 부호’라는 체계가 있어서, 누구나 미리 약속된 규칙에 따라서 문자를 인코드하고 디코드할 수 있습니다. 누구는 A01 로 표시하고, 누구는 011 로 표시하면 서로 읽을 수 없겠죠.

컴퓨터도 문자를 나타내기 위해서 각 문자들을 숫자로 대응시킵니다. ‘A’는 65, ‘B’는 66, ‘a’는 97, ‘b’는 98 이런 식으로요. ‘모르스 부호’처럼, 컴퓨터에서 사용하는 부호 체계 중 대표적인 것으로 ASCII(아스키 코드: American Standard Code for Information Interchange)가 있습니다.

https://upload.wikimedia.org/wikipedia/commons/thumb/1/1b/ASCII-Table-wide.svg/1000px-ASCII-Table-wide.svg.png

이러한 부호 체계의 종류를 ‘인코딩(encoding)’이라고 부릅니다. 그런데 ASCII는 미국에서 만들어졌기 때문에, 영어에서 사용하는 알파벳과 몇 가지의 기호들만 포함되어 있습니다. 그러면 한글은 어떻게 표시할까요? 한글 역시 한글을 사용하는 사람들끼리 일정한 코드표를 만들어서 정해진 규칙에 따라 코드로 표현하고, 또 코드로 표현된 것을 한글로 간주하고 읽으면 되겠죠.

그런데 여기서, 컴퓨터와 한글 처리의 역사가 개입합니다. 영문자와는 달리 한글은 초-중-종성이 존재합니다. 그래서 표현할 수 있는 글자의 수도 많죠. (물론 한자는 그보다 더 많겠습니다만.) 그래서인지, 한글을 어떻게 효과적으로 인코딩할 것인가, 즉 코드로 어떻게 표현할 것인가에 대해 오랜 기간에 걸쳐 여러 방식들이 제안되었습니다. 다양한 방식들이 80~90년대에 제안되고 사라졌습니다. 그리고 현재까지 남아서 널리 사용되는 한글 인코딩 방식은 대표적으로 다음과 같은 것들이 있습니다:

  • EUC-KR
  • UTF-8 (Unicode)

다양한 국가에서 각자의 언어를 위한 나름대로의 인코딩을 정의해서 사용하면서, 또 인터넷이 점차 발달하여 다양한 국가의 사람들이 서로 문자를 교환할 필요가 생기기 시작하면서, 인코딩에도 국제 표준을 정하자는 움직임이 발생했는데, 그 결과물이 유니코드(Unicode)입니다. 그래서 최근에는 한글을 유니코드, 그 중에서도 UTF-8을 사용해서 저장하는 것을 권장합니다. (간혹, 아이콘(emoji)을 표현하기 위해서는 UTF-32를 사용해야 하는 경우도 있습니다.)

하지만 옛날에 만들어진 자료들, 특히 텍스트 파일(.TXT)이나 웹페이지 같은 경우는 여전히 EUC-KR로 저장되어 있는 경우도 많이 있습니다. 나중에 혹시 데이터를 읽어들인 후에 한글이 깨져서 보인다면, 인코딩을 다르게 지정해서 읽어들여보세요.

인코딩이 잘못 지정되어 한글이 제대로 보이지 않을만한 상황은 아래와 같은 경우가 있겠습니다:

  • HNC 한글, MS Word, MS Excel 등, 애플리케이션의 데이터 파일이 아닌 일반 텍스트 에디터로 작성한 내용을 읽어들일 때
  • 애플리케이션에서 파일을 TXT나 CSV 등의 일반 텍스트 형태로 저장하고 그것을 불러들일 때
  • 웹문서를 읽어들일 때

byte

byte는 인간의 문자로 인식하기 전 단계로, 컴퓨터가 인식할 수 있는 데이터입니다. 컴퓨터는 기본적으로 0과 1만을 인식할 수 있습니다. 전압이 높으면 1, 낮으면 0으로 표현하는 등, 일련의 약속을 정해놓고, 모든 정보를 0과 1로 표현합니다. 이것을 비트(bit)라고 부릅니다. 그런데 이렇게 0-1만으로 정보를 표시하면, 사람이 사용하기에는 번거롭습니다. 0과 1로 표현하는 것을 2진법이라고 한다면, 사람이 사용하기에는 10진법을 사용하는 것이 가장 좋겠지요. 하지만 컴퓨터는 2진법을 사용하기 때문에, 2의 승수로 표현할 수 있는 진법을 사용해야 합니다. 그래서 2진법, 4진법, 8진법, 16진법, 32진법 등을 사용할 수 있죠. 그 중에서 컴퓨터의 역사에서는 16진법(Hexadecimal)을 택하기로 결정합니다.

16진법을 표기하는 것은, 처음에는 10진법과 같습니다. 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 까지는요. 그런데 10진법에서는, 9 다음은 한 자리가 올림되어 1+0 이 됩니다. 그래서 10 이죠. 하지만 16진법은 6개의 수를 더 표현해야 하죠. 그래서 A, B, C, D, E, F 의 알파벳을 동원하여 표현합니다. 즉, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F 까지가 16진법에서의 숫자가 됩니다. F 다음은, 1+0, 즉 10 이 됩니다. 16진법에서의 10 은, 10진법에서의 16 과 동일합니다.

Python에서 byte는 b'\x01'= 처럼 표시됩니다. 문자열과 비슷한데, 앞에 =b 라는 접두어가 붙고, 내용에도 \x 라는 접두어 뒤에 실제 16진수를 적어줍니다.

'안녕'.encode('utf8') == b'\xec\x95\x88\xeb\x85\x95'
bytes.fromhex('ec 95 88 eb 85 95') == b'\xec\x95\x88\xeb\x85\x95'

평소에는 byte를 직접 다룰 일은 없겠지만, 파일로부터 읽어들이거나, 특히 웹에서 문서를 가져올 때, 결과값이 byte로 오는 경우가 있습니다. 그럴 때는 적절한 인코딩을 선택하여 문자열로 바꾸어주면 됩니다.

연습문제: 단어 갯수 세기

아래와 같이 주어진 문서 내에서 unique한 단어가 몇 개인지 세어봅시다.

# -*- coding: utf-8 -*-

text = '프로그램 언어를 익히기 위해 책이나 글만 보면서 따라해서는 중간에 막히는 부분들이 발생합니다. 그리고 막연히 어렵게 느껴지기도 하고요. 또 어떤 경우에는 눈으로만 읽는 분들이 있는데, 눈으로만 봐서는 실제로 프로그램을 작성하기가 어렵습니다. 본 과정은 실습을 중심으로 진행합니다. 그래서, 따라할 수 있는 형태의 강의 자료가 제공됩니다. 온라인에 공개되기 때문에 수업을 듣지 않은 분들도 자료를 열람할 수 있지만, 실습을 진행하면서 발생하는 Q&A나 개별 1:1 지도, 각 개인의 프로젝트 목표에 대한 피드백 등은 제한된 메일링 리스트를 사용하여 진행합니다.'

new_text = text.replace(',', '').replace('.', '')
word_list = new_text.split()
word_unique_set = set()

for word in word_list:
    word_unique_set.add(word)

print('Total words: {}'.format(len(word_unique_set)))
Total words: 69

위의 예제를 변형해서, 각 단어의 빈도가 어떻게 되는지 세어보는 프로그램을 작성해보세요. dict 를 활용해보세요.

연습문제: 의미망 그리기

가끔 방송에서 텍스트를 바탕으로 의미망 분석을 하는 경우가 있죠.

지금까지 배운 것을 바탕으로, ‘문장 동시출현 빈도’를 활용한 아주 초보적인 의미망 분석을 한번 해보도록 하겠습니다.

원래 의미망 분석을 하려면, 문장에 대해 형태소 분석을 하고, 접속사나 부사 등 불필요한 말들은 제거하는 등의 작업을 거칩니다. 하지만, 여기서는 단순히 어절 단위로만 잘라서, 의미망을 이런 식으로 그리는구나 하는 정도만 맛보도록 하겠습니다.

의미망을 본격적으로 그리기 전에, for 구문이 중첩된 경우 어떻게 실행되는지 살펴보겠습니다.

for i in [0, 1, 2]:
    for j in [0, 1, 2]:
        print(i, j)
0 0
0 1
0 2
1 0
1 1
1 2
2 0
2 1
2 2

위 코드의 실행 순서를 살펴볼까요?

1:  for i in [0, 1, 2]:
2:      for j in [0, 1, 2]:
3:          print(i, j)

위 코드는 아래와 같은 순서대로 실행됩니다.

  • 1 → 2 → 3 → 2 → 3 → 2 → 3
  • 1 → 2 → 3 → 2 → 3 → 2 → 3
  • 1 → 2 → 3 → 2 → 3 → 2 → 3

3행은 총 9번 실행됩니다. 바깥의 i를 가진 for문이 3회, 안쪽의 j를 가진 for문이 3회 순회하기 때문에, 3행은 3*3=9회 실행됩니다.

세번 중첩된 for 문은 어떻게 실행될까요?

for i in [0, 1, 2]:
    for j in [0, 1, 2]:
        for k in [0, 1]:
            print(i, j, k)
0 0 0
0 0 1
0 1 0
0 1 1
0 2 0
0 2 1
1 0 0
1 0 1
1 1 0
1 1 1
1 2 0
1 2 1
2 0 0
2 0 1
2 1 0
2 1 1
2 2 0
2 2 1

위 코드의 실행 순서를 알아볼까요?

1:  for i in [0, 1, 2]:
2:      for j in [0, 1, 2]:
3:          for k in [0, 1]:
4:              print(i, j, k)
  • 1 → 2 → 3 → 4 → 3 → 4 → 2 → 3 → 4 → 3 → 4 → 2 → 3 → 4 → 3 → 4
  • 1 → 2 → 3 → 4 → 3 → 4 → 2 → 3 → 4 → 3 → 4 → 2 → 3 → 4 → 3 → 4
  • 1 → 2 → 3 → 4 → 3 → 4 → 2 → 3 → 4 → 3 → 4 → 2 → 3 → 4 → 3 → 4

4행은 총 몇회 실행될까요? 첫번째 for 문이 순회할 횟수는 3번, 두번째 for 문은 3번, 세번째 for 문은 2번, 이렇게 해서 3*3*2 = 18회 실행됩니다.

우선, 의사 코드(psudo code)로 먼저 어떤 작업을 할 것인지 알아보겠습니다.

# 분석할 텍스트를 가져온다
# 텍스트를 줄 단위로 잘라 리스트로 만든다 (split)
# 링크(단어,단어)의 빈도를 담을 빈 dict를 만든다
#
# 각 행을 돌면서 수행한다 (for)
#   해당 행에서 앞뒤 공백문자를 제거한다 (strip)
#   만약 빈 줄이면 건너뛴다 (if, continue)
#   해당 행을 마침표(.)를 기준으로 문장 단위로 잘라 리스트로 만든다 (split)
#
#   각 문장을 돌면서 수행한다 (for)
#      만약 해당 문장이 빈 문장이면 건너뛴다 (if, continue)
#      해당 문장을 공백 기준으로 단어 단위로 잘라 리스트로 만든다 (split)
#      각 단어에서 구두점이나 공백을 없앤다 (replace, strip, list comprehension)
#      구두점이나 공백 제거로 인해 빈 문자열이 된 단어, 그리고 한글자 단어를 없앤다 (len, list comprehension)
#
#      각 단어의 갯수만큼 돌면서 수행한다 (for i)
#         각 단어의 갯수만큼 돌면서 수행한다 (for j)
#            단어 i와 단어 j를 튜플로 만들어 링크로 표현한다
#            단어 i와 단어 j를 정렬하여 링크 표현을 일관되게 한다. ('되겠습니다', '대통령이') => ('대통령이', '되겠습니다')
#            해당 링크에 대한 빈도를 하나 증가시킨다
# 
# 단어 링크 빈도 dict의 모든 key를 가져온다 (dict.keys())
# 각 key를 돌면서 수행한다 (for)
#   만약 key에 해당하는 단어의 빈도가 2보다 작다면 (if)
#     해당 링크 key를 dict에서 제거한다 (del)
#
# 단어 링크 빈도 dict의 정보를 graph에 옮겨 담는다
# 그래프를 그린다

본문은 문재인 대통령의 대통령 취임 연설문입니다.

# -*- coding: utf-8 -*-

import networkx as nx
import matplotlib.pyplot as plt

text = '''존경하고 사랑하는 국민 여러분, 감사합니다. 국민 여러분의 위대한 선택에 머리숙여 깊이 감사드립니다.\n\n저는 오늘 대한민국 제19대 대통령으로서 새로운 대한민국을 향해 첫걸음을 내딛습니다. 지금 제 두 어깨는 국민 여러분으로부터 부여받은 막중한 소명감으로 무겁습니다. 지금 제 가슴은 한번도 경험하지 못한 나라를 만들겠다는 열정으로 뜨겁습니다. 그리고 지금 제 머리는 통합과 공존의 새로운 세상을 열어갈 청사진으로 가득차 있습니다.\n\n우리가 만들어가려는 새로운 대한민국은 숱한 좌절과 패배에도 불구하고 우리의 선대들이 일관되게 추구했던 나라입니다. 또 많은 희생과 헌신을 감내하며 우리 젊은이들이 그토록 이루고 싶어했던 나라입니다. 그런 대한민국을 만들기 위해 저는 역사와 국민 앞에 두렵지만 겸허한 마음으로 대한민국 제19대 대통령으로서의 책임과 소명을 다할 것임을 천명합니다.\n\n함께 선거를 치른 후보들께 감사의 말씀과 함께 심심한 위로를 전합니다. 이번 선거에서는 승자도 패자도 없습니다. 우리는 새로운 대한민국을 함께 이끌어가야 할 동반자입니다. 이제 치열했던 경쟁의 순간을 뒤로하고 함께 손을 맞잡고 앞으로 전진해야합니다.\n\n존경하는 국민 여러분, 지난 몇달 우리는 유례없는 정치적 격변기를 보냈습니다. 정치는 혼란스러웠지만 국민은 위대했습니다. 현직 대통령의 탄핵과 구속앞에서도 국민들이 대한민국의 앞길을 열어주셨습니다. 우리 국민들은 좌절하지 않고 오히려 이를 전화위복의 계기로 승화시켜 마침내 오늘 새로운 세상을 열었습니다. 대한민국의 위대함은 국민의 위대함입니다.\n\n그리고 이번 대선에서 우리국민들은 또 하나의 역사를 만들어주셨습니다. 전국 각지에서 골고른 지지로 새로운 대통령을 선택해주셨습니다.\n\n오늘부터 저는 국민 모두의 대통령이 되겠습니다. 저를 지지하지 않았던 국민 한 분 한 분도 저의 국민이고, 우리의 국민으로 섬기겠습니다.\n\n저는 감히 약속드립니다. 2017년 5월10일, 이날은 진정한 국민 통합이 시작되는 예로 역사에 기록될 것입니다.\n\n존경하고 사랑하는 국민 여러분, 힘들었던 지난 세월 국민들은 이게 나라냐고 물었습니다. 대통령 문재인은 바로 그 질문에서 새로 시작하겠습니다. 오늘부터 나라를 나라답게 만드는 대통령이 되겠습니다.\n\n구시대의 잘못된 관행과 과감히 결별하겠습니다. 대통령부터 새로워지겠습니다.\n\n우선 권위적 대통령 문화를 청산하겠습니다. 준비를 마치는 대로 지금의 청와대에서 나와 광화문 대통령 시대를 열겠습니다. 참모들과 머리와 어깨를 맞대고 토론하겠습니다. 국민과 수시로 소통하는 대통령이 되겠습니다. 주요 사안은 대통령이 직접 언론에 브리핑하겠습니다.\n\n퇴근길에는 시장에 들러 마주치는 시민들과 격의없는 대화를 나누겠습니다. 때로는 광화문광장에서 대토론회를 열겠습니다. 대통령의 제왕적 권력을 최대한 나누겠습니다. 권력기관은 정치로부터 완전히 독립시키겠습니다. 그 어떤 권력기관도 무소불위 권력행사를 하지 못하게 견제장치를 만들겠습니다.\n\n낮은 자세로 일하겠습니다. 국민과 눈높이를 맞추는 대통령이 되겠습니다.\n\n안보위기도 서둘러 해결하겠습니다. 한반도 평화를 위해 동분서주하겠습니다. 필요하면 곧바로 워싱턴으로 날아가겠습니다. 베이징과 도쿄에도 가고. 여건이 조성되면 평양에도 가겠습니다.\n\n한반도 평화정착을 위해서라면 제가 할 수 있는 모든 일을 다하겠습니다.\n\n한미동맹은 더욱 강화하겠습니다. 한편으로 사드문제 해결을 위해 미국 및 중국과 진지하게 협상하겠습니다.\n\n튼튼한 안보는 막강한 국방력에서 비롯됩니다. 자주국방력 강화를 위해 노력하겠습니다.\n\n북핵 문제를 해결할 토대도 마련하겠습니다. 동북아 평화구조를 정착시켜 한반도 긴장완화의 전기를 마련하겠습니다.\n\n분열과 갈등의 정치도 바꾸겠습니다. 보수와 진보의 갈등은 끝나야 합니다. 대통령이 나서서 직접 대화하겠습니다. 야당은 국정운영의 동반자입니다. 대화를 정례화하고 수시로 만나겠습니다.\n\n전국적으로 고르게 인사를 등용하겠습니다. 능력과 적재적소를 인사의 대원칙으로 삼겠습니다. 저에 대한 지지 여부와 상관없이 유능한 인재를 삼고초려해 일을 맡기겠습니다.\n\n나라 안팎으로 경제가 어렵습니다. 민생도 어렵습니다. 선거 과정에서 약속했듯이 무엇보다 먼저 일자리를 챙기겠습니다. 동시에 재벌개혁에도 앞장서겠습니다. 문재인 정부 하에서는 정경유착이란 낱말이 완전히 사라질 것입니다.\n\n지역과 계층과 세대간 갈등을 해소하고 비정규직 문제도 해결의 길을 모색하겠습니다. 차별없는 세상을 만들겠습니다.\n\n거듭 말씀드립니다. 문재인과 더불어민주당정부에서 기회는 평등할 것입니다. 과정은 공정할 것입니다. 결과는 정의로울 것입니다.\n\n존경하는 국민 여러분, 이번 대통령선거는 전임 대통령의 탄핵으로 치러졌습니다. 불행한 대통령의 역사가 계속되고 있습니다. 이번 선거를 계기로 이 불행한 역사는 종식돼야 합니다.\n\n저는 대한민국 대통령의 새로운 모범이 되겠습니다. 국민과 역사가 평가하는 성공한 대통령이 되기 위해 최선을 다하겠습니다. 그래서 지지와 성원에 보답하겠습니다.\n\n깨끗한 대통령이 되겠습니다. 빈손으로 취임하고 빈손으로 퇴임하는 대통령이 되겠습니다. 훗날 고향으로 돌아가 평범한 시민이 되어 이웃과 정을 나눌 수 있는 대통령이 되겠습니다. 국민 여러분의 자랑으로 남겠습니다.\n\n약속을 지키는 솔직한 대통령이 되겠습니다. 선거 과정에서 제가 했던 약속들을 꼼꼼하게 챙기겠습니다. 대통령부터 신뢰받는 정치를 솔선수범해야 진정한 정치발전이 가능할 것입니다. 불가능한 일을 하겠다고 큰소리치지 않겠습니다. 잘못한 일은 잘못했다고 말씀드리겠습니다. 거짓으로 불리한여론을 덮지 않겠습니다. 공정한 대통령이 되겠습니다.\n\n특권과 반칙이 없는 세상을 만들겠습니다. 상식대로 해야 이득을 보는 세상을 만들겠습니다. 이웃의 아픔을 외면하지 않겠습니다. 소외된 국민이 없도록 노심초사하는 마음으로 항상 살피겠습니다.\n\n국민들의 서러운 눈물을 닦아드리는 대통령이 되겠습니다. 소통하는 대통령이 되겠습니다. 낮은 사람, 겸손한 권력이 돼 가장 강력한 나라를 만들겠습니다. 군림하고 통치하는 대통령이 아니라 대화하고 소통하는 대통령이 되겠습니다.\n\n광화문시대 대통령이 되어 국민과 가까운 곳에 있겠습니다. 따뜻한 대통령, 친구같은 대통령으로 남겠습니다.\n\n사랑하고 존경하는 국민 여러분, 2017년 5월10일 오늘 대한민국이 다시 시작합니다. 나라를 나라답게 만드는 대역사가 시작됩니다. 이 길에 함께해 주십시오. 저의 신명을 바쳐 일하겠습니다. 감사합니다.'''

lines = text.split('\n')      # 줄 단위로 자른다

word_edges = {}

for line in lines:
    _line = line.strip()
    if not _line:             # 빈줄이면 건너뛴다
        continue
    statements = _line.split('.') # 문장 단위로 자른다
    for statement in statements: # 빈 문장이면 건너뛴다
        if not statement:
            continue
        words = statement.split(' ') # 단어 단위로 자른다
        cleansed_words = [w.replace('.', '').replace(',', '').strip() for w in words] # 단어에서 구두점이나 공백을 없앤다
        cleansed_words_2 = [w for w in cleansed_words if len(w) > 1] # 구두점 및 공백 제거로 인해 빈 문자열이 되어버린 원소, 그리고 한글자 단어를 제거한다
        num_words = len(cleansed_words_2)
        for index_i in range(num_words): # 한 문장에 등장한 단어들을 서로 연결한다
            word_i = cleansed_words_2[index_i]
            for index_j in range(index_i+1, num_words):
                word_j = cleansed_words_2[index_j]
                word_to_word = (word_i, word_j)
                word_to_word = tuple(sorted(word_to_word))
                word_edges[word_to_word] = word_edges.setdefault(word_to_word, 0) + 1

# 등장 빈도가 1회인 edge는 제거한다
keys = list(word_edges.keys())
for key in keys:
    if word_edges[key] < 2:
        del word_edges[key]

G = nx.Graph()
for (word_1, word_2), freq in word_edges.items():
    G.add_edge(word_1, word_2, weight=freq)

pos = nx.kamada_kawai_layout(G)
plt.figure(figsize=(12, 12))    # 결과 이미지 크기를 크게 지정 (12inch * 12inch)
widths = [G[node1][node2]['weight'] for node1, node2 in G.edges()]
nx.draw_networkx_edges(G, pos, width=widths, alpha=0.1);
nx.draw_networkx_labels(G, pos, font_family='Noto Sans CJK KR'); # 각자 시스템에 따라 적절한 폰트 이름으로 변경
plt.show()

outputs/language-basic-2-word-map.png