Skip to content

Latest commit

 

History

History
915 lines (677 loc) · 37.1 KB

File metadata and controls

915 lines (677 loc) · 37.1 KB

Python 기초 3: 파일 다루기, 함수, 클래스

함수 (Function)

지금까지 프로그램을 만들기 위한 하나 하나의 조각들을 살펴봤다면, 이제는 그 조각들을 쌓아올리기 위해, 다시 말해서 구조화를 하기 위해서 필요한 도구들을 살펴봅시다.

우선, 가장 기본적인 도구로는 함수가 있습니다. 수학적인 측면에서 함수는 ‘어떤 입력 X에 대해 Y라는 값을 결과로 되돌려주는 것’이라고 표현할 수 있겠죠. 기하적으로 표현하면 ‘X의 한 점을 Y라는 점으로 변환(mapping)해주는 것’이라고 할 수 있겠고요.

Python에서는 아래와 같이 함수를 정의할 수 있습니다.

def add(x, y):
    return x + y

print(add(1, 3))

위 함수는, x, y 라는 값을 받아서 둘을 더한 값을 반환해줍니다. 함수 정의 형식은 아래와 같이 표현할 수 있겠습니다.

def 함수이름(인자1, 인자2, 인자3, ...):
    # 내용
    return

하지만, 프로그래밍에서 함수는, 수학적인 의미에 더해서, ‘여러 개의 일련의 연관된 작업을 의미 있는 단위로 묶는다’는 의미를 가집니다. 이를테면 여러 연관된 문장을 묶은 뒤, 소제목을 붙인다고 할까요.

첫날부터 사용했던, 그래프 그리는 예제 코드를 한번 살펴봅시다.

import networkx as nx

G = nx.Graph()
G.add_node(1)
G.add_node(2)
G.add_edge(1, 2)

pos = nx.spring_layout(G)
nx.draw_networkx_edges(G, pos, alpha=0.2);
nx.draw_networkx_nodes(G, pos, node_size=50);

여기서, 네트워크에 노드와 링크를 추가하는 부분 이외에, 실제로 네트워크를 그리는 부분의 코드가 좀 복잡하여 숨기고 싶을 때, 함수를 사용할 수 있습니다.

import networkx as nx

def draw_graph(graph):
    pos = nx.spring_layout(graph)
    nx.draw_networkx_edges(graph, pos, alpha=0.2)
    ax = nx.draw_networkx_nodes(graph, pos, node_size=50)
    return ax

G = nx.Graph()
G.add_node(1)
G.add_node(2)
G.add_edge(1, 2)
# 우리가 만든 함수를 호출합니다.
draw_graph(G);

G2 = nx.Graph()
G2.add_node(1)
G2.add_node(2)
G2.add_node(3)
G2.add_edge(1, 2)
G2.add_edge(1, 3)
G2.add_edge(2, 3)
# 우리가 만든 함수를 호출합니다.
draw_graph(G2);

이제 복잡한 그리기 명령들을 draw_graph() 라는 함수를 호출하는 것으로 해결할 수 있습니다.

그런데, 투명도나 노드의 크기를 네트워크마다 다르게 지정하고 싶을 수 있습니다. 하지만 지금은 투명도나 노드의 크기가 함수 안에 고정되어 있어서 네트워크마다 다르게 지정할 수 없네요. 이것을 필요에 따라 변경할 수 있도록 만들어볼까요?

import networkx as nx

def draw_graph(graph, alpha, node_size):
    pos = nx.spring_layout(graph)
    nx.draw_networkx_edges(graph, pos, alpha=alpha)
    ax = nx.draw_networkx_nodes(graph, pos, node_size=node_size)
    return ax

G = nx.Graph()
G.add_node(1)
G.add_node(2)
G.add_edge(1, 2)
# 우리가 만든 함수를 호출합니다. 투명도와 노드 크기를 지정합니다.
draw_graph(G, 0.2, 50);

G2 = nx.Graph()
G2.add_node(1)
G2.add_node(2)
G2.add_node(3)
G2.add_edge(1, 2)
G2.add_edge(1, 3)
G2.add_edge(2, 3)
# 우리가 만든 함수를 호출합니다. 투명도와 노드 크기를 지정합니다.
draw_graph(G2, 0.5, 100);

위와 같이 draw_graph() 함수에 graph 라는 인자 외에도 alpha, node_size 라는 인자값을 줄 수 있도록 함수 정의를 확대했습니다. 그런데 이번에는 draw_graph() 함수를 호출하여 사용할 때마다 매번 graph, alpha, node_size 세 인자를 꼬박꼬박 넣어줘야 하는 것이 불편합니다. alpha, node_size 인자는 주로 많이 쓰는 값이 정해져 있고, 예외적인 경우에만 다르게 지정할 수 있게 하면 좋겠습니다.

import networkx as nx

def draw_graph(graph, alpha=0.2, node_size=50):
    pos = nx.spring_layout(graph)
    nx.draw_networkx_edges(graph, pos, alpha=alpha)
    ax = nx.draw_networkx_nodes(graph, pos, node_size=node_size)
    return ax

G = nx.Graph()
G.add_node(1)
G.add_node(2)
G.add_edge(1, 2)
# 우리가 만든 함수를 호출합니다. 투명도와 노드 크기를 생략합니다.
draw_graph(G);

G2 = nx.Graph()
G2.add_node(1)
G2.add_node(2)
G2.add_node(3)
G2.add_edge(1, 2)
G2.add_edge(1, 3)
G2.add_edge(2, 3)
# 우리가 만든 함수를 호출합니다. 투명도와 노드 크기를 지정합니다.
draw_graph(G2, 0.5, 100);

위와 같이 함수를 선언할 때 인자에 기본값을 부여하면, 함수를 호출할 때 기본값이 부여된 인자들을 생략할 수 있습니다.

함수가 어떤 것인지 실제 사례를 살펴보았습니다.

함수 자체에 대해서 조금 더 살펴봅시다.

Positional argument vs Keyword argument

앞의 예에서, remove_low_frequency() 함수의 인자 중 cutoff 인자가 약간 다르게 생긴 것을 볼 수 있습니다.

def draw_graph(graph, alpha=0.2, node_size=50):
    return

graph 와 같은, = 표시가 없이 정의된 인자를 위치 인자라고 하고, alpha 와 같이 = 표시와 함께 값이 정의된 인자를 키워드 인자라고 합니다.

위치 인자는 위치에 의해서 변수가 할당됩니다. 아래와 같은 함수가 있다고 할 때, 주어지는 인자의 순서에 따라서 함수를 정의할 때 지정된 변수에 값이 들어갑니다.

def positional(arg1, arg2):
    print('I am', arg1, arg2)

positional('a', 'boy')
positional('boy', 'a')
I am a boy
I am boy a

반면, alpha 와 같은 키워드 인자는 이름을 지정해서 부여할 수 있습니다. 이름을 부여하지 않으면 위치 인자와 동일하게 동작합니다.

def keyword(kwarg1=None, kwarg2='girl'):
    print('I am', kwarg1, kwarg2)

keyword('a', 'boy')
keyword('a', kwarg2='boy')
keyword(kwarg1='a', kwarg2='boy')
keyword(kwarg2='boy', kwarg1='a')
keyword(kwarg1='a')
I am a boy
I am a boy
I am a boy
I am a boy
I am a girl

또한, 위치 인자는 생략이 불가능하지만, 키워드 인자는 생략이 가능합니다. 키워드 인자를 생략할 경우, 함수를 정의할 때 지정했던 기본값이 변수에 할당됩니다.

그리고, 위치 인자와 키워드 인자를 하나의 함수 선언에 동시에 사용할 수 있습니다. 하지만 그 경우에는, 위치 인자를 앞쪽에, 그리고 키워드 인자를 뒷쪽에 선언해야 합니다. 키워드 인자가 위치 인자보다 앞에 선언될 수 없습니다.

def mixed(arg1, arg2, kwarg1='studying', kwarg2='excel'):
    print(arg1, arg2, kwarg1, kwarg2)

mixed('I', 'am', kwarg2='python')
I am studying python
def mixed2(kwarg0=None, arg1, arg2, kwarg1='studying', kwarg2='excel'): # 키워드 인자가 위치 인자의 앞에 선언될 수 없음
    return

mixed('I')                      # 위치 인자는 생략할 수 없음 (arg2가 주어지지 않았음)

Argument list unpacking

그런데 가만히 살펴보면, 위치 인자는 listtuple 과 닮아있고, 키워드 인자는 dict 와 닮아있습니다. listtuple 도 위치로만 각 원소를 지칭할 수 있고, dict 는 이름으로 부를 수 있습니다.

이런 특성을 활용해서, 인자를 직접 코드에 기재하지 않고도, listtuple, dict 를 사용해서 부여할 수 있습니다.

def mixed(arg1, arg2, kwarg1='studying', kwarg2='excel'):
    print(arg1, arg2, kwarg1, kwarg2)

args = ['I', 'am']
mixed(*args)

args = ['We', 'are']
mixed(*args)

kwargs = {'kwarg2': 'python'}
mixed(*args, **kwargs)
I am studying excel
We are studying excel
We are studying python

위와 같이, 위치 인자로 전달할 값들을 listtuple 에 넣고, 함수에 넣어줄 때는 변수 이름 앞에 * 을 붙여주면, , 를 사용해서 연속된 값을 넣어준 것과 같은 효과가 납니다.

비슷하게, 키워드 인자로 전달할 값들을 dict 에 인자의 이름과 함께 넣고, 변수 이름 앞에 ** 를 붙여주면, 이름을 지정해서 값을 넣어준 것과 같은 효과가 납니다.

First-class function & lambda function

다음으로 살펴볼 것은, Python의 일급 함수 언어로서의 특성입니다. first-class function 이라고 하는데, 함수를 일급 시민으로 취급한다는 이야기입니다. 프로그래밍 언어에서 일급 시민으로 취급한다는 이야기는, 함수를 변수에 저장하거나 함수의 인자로 전달하는 등, 일반적인 값처럼 동일하게 다룰 수 있다는 것을 뜻합니다.

설명하자면 복잡하니, 다음 예를 보시죠.

a_list = 1.3453
a_function = round              # 변수에 round 함수를 저장. 함수 이름만으로 지칭.
print(a_function(a_list))       # 변수에 저장된 함수를 실행
1

위와 같이, Python에서는 함수를 변수에 저장할 수도 있고, 변수에 저장된 함수를 실행할 수도 있습니다.

그러면 이런 기능이 어디에서 쓸모가 있을까요? 아래와 같은 사례를 생각해봅시다.

a_list = [1.3453, 4.33240, 4.38273, 2.2381, -30.3942]

def clean_data(lst, func=round):
    return list([func(element) for element in lst])

print(clean_data(a_list))
print(clean_data(a_list, func=abs))
[1, 4, 4, 2, -30]
[1.3453, 4.3324, 4.38273, 2.2381, 30.3942]

이후에 더 자세히 살펴보겠지만, pandas 라이브러리를 사용할 때에 이 기능을 활용할 일이 생깁니다.

import pandas as pd

df = pd.DataFrame([1.3453, 4.33240, 4.38273, 2.2381, -30.3942])
rounded_df = df.apply(round)
rounded_df
0
01
14
24
32
4-30

그런데 round 함수는 인자를 가질 수 있습니다. 소숫점 몇자리에서 반올림할 것인지 결졍할 수 있습니다. 이런 경우는 어떻게 해야 할까요? 우선 아래와 같이 별도의 함수를 정의해서 사용할 수 있겠습니다.

import pandas as pd

def round_two_decimal_point(val):
    return round(val, 2)

df = pd.DataFrame([1.3453, 4.33240, 4.38273, 2.2381, -30.3942])
rounded_df = df.apply(round_two_decimal_point)
rounded_df
0
01.35
14.33
24.38
32.24
4-30.39

그런데 이렇게 한 번만 사용할 함수를 정의하자니 함수를 정의하는 구문이 간단하기는 하지만 다소 거추장스럽습니다. 이럴 때는 람다(lambda) 함수를 사용할 수 있습니다. 람다 함수는, 이름이 없는 함수로, 간편하게 함수를 정의할 수 있습니다. 일반적으로 def <funcname>(args...): 형태로 함수를 정의하는 대신, 람다 함수는 lambda arg...: 형태로 함수를 간결하게 정의합니다.

위의 코드를 단계적으로 람다 함수로 대체하면 아래와 같습니다.

import pandas as pd

round_two_decimal_point = lambda val: round(val, 2)

df = pd.DataFrame([1.3453, 4.33240, 4.38273, 2.2381, -30.3942])
rounded_df = df.apply(round_two_decimal_point)
rounded_df
import pandas as pd

df = pd.DataFrame([1.3453, 4.33240, 4.38273, 2.2381, -30.3942])
rounded_df = df.apply(lambda val: round(val, 2))
rounded_df
0
01.35
14.33
24.38
32.24
4-30.39

이해가 잘 되지 않으면, 그냥 이름 있는 함수를 사용해도 됩니다.

함수를 활용한 코드 정리 사례

이전에 의미망 예제가 조금 복잡했었죠? 그걸 함수를 사용해서 묶어보겠습니다.

import networkx as nx
import matplotlib.pyplot as plt

def read_file(path):
    with open(path) as fin:
        return fin.read()

def construct_wordnet(text):
    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
    return word_edges

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

def draw_graph(word_edges):
    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') # 각자 시스템에 따라 적절한 폰트 이름으로 변경
    return

크게 세 부분으로 나누고 소제목(함수 이름)을 붙여주었습니다.

  • 텍스트로부터 의미망 데이터를 만들어내는 부분
  • 빈도가 적은 링크는 제거하는 부분
  • 그래프를 그리는 부분

그리고, 데이터를 파일로부터 읽어들이는 함수를 하나 추가해봅시다.

def read_file(path):
    with open(path) as fin:
        return fin.read()

이제 다양한 문서에 대해서 아래와 같은 몇 줄의 코드만으로 의미망을 출력할 수 있게 되었습니다.

text = read_file('assets/moon_speech.txt')
wordnet = construct_wordnet(text)
remove_low_frequency(wordnet)
draw_graph(wordnet)
plt.show()

outputs/moon_speech.png

박근혜 전 대통령 취임사에 대해서도 의미망을 살펴봅시다.

text = read_file('assets/park_speech.txt')
wordnet = construct_wordnet(text)
remove_low_frequency(wordnet)
draw_graph(wordnet)
plt.show()

outputs/park_speech.png

박근혜 대통령은 좀 더 다양한 어휘를 사용한 것 같네요. cutoff 를 좀 더 강화해봅시다.

text = read_file('assets/park_speech.txt')
wordnet = construct_wordnet(text)
remove_low_frequency(wordnet, cutoff=3)
draw_graph(wordnet)
plt.show()

outputs/park_speech_2.png

클래스 (Class)

다음으로 살펴볼 것은 클래스입니다. 여러분이 직접 클래스를 만들어서 사용할 일은 별로 없을 것이라고 생각합니다. 하지만 외부 라이브러리은 클래스로 작성된 경우가 많습니다. 때문에 간단하게 클래스가 어떤 것인지 살펴보고, 어떻게 이해하고 사용하면 되는지 살펴보겠습니다.

클래스는 기본적으로 함수와 비슷한데, 여러 개의 함수가 하나의 묶음으로 묶여있는 것이 특징입니다. 클래스로 묶여있는 함수를 메소드(method)라고 부릅니다. 뿐만 아니라, 하나의 클래스 안에는 여러 함수들에서 공통적으로 사용하는 변수들이 존재합니다.

기본적으로, 클래스는 아래와 같이 정의하고 사용합니다.

from datetime import datetime
from random import randint
from random import random

class Person:
   def __init__(self, name, weight=3.0, height=0.20):
      self.name = name
      self.weight = weight
      self.height = height

   def eat(self):
      self.weight = self.weight + round(random(), 2) * 10

   @property
   def bmi(self):
      return round(self.weight / pow(self.height, 2), 1)

p1 = Person('Tom', weight=75.0, height=1.83)
print(p1.bmi)
p1.eat()
p1.eat()
print(p1.bmi)
22.4
25.5

클래스 정의는 크게 아래와 같은 모양을 따릅니다.

class 클래스이름:
    def __init__(self, arg1, arg2, ...):
        self.arg1 = arg1
        self.arg2 = arg2

    def 메소드이름1(self):
        return

여기서 __init__() 메소드는 특별한 용도의 메소드로서, 생성자라고 부릅니다. 생성자 메소드는 클래스로부터 객체(object)를 생성할 때, 속성값, 즉 객체 변수들에게 초기값을 부여하기 위해 사용됩니다. 위에서 정의한 Person 클래스에서는 생성자가 name 이라는 위치 인자, weight, height 이라는 키워드 인자를 가집니다.

클래스를 사용하기 위해서는 대부분의 경우 객체(object)로 변환한 뒤 사용합니다. 여러분이 설문조사를 할 때를 생각해보면 될 것 같은데요. 설문 양식을 설계한 다음에, 실제로 설문지를 돌려서 설문을 합니다. 여기서 설문 양식=클래스, 그리고 실제 응답이 적힌 설문지=객체라고 이해할 수 있습니다.

사용하는 측면에서는 이것 자체에 대해 깊이 이해할 필요는 없고, 이 정도만 이해하시면 됩니다. 클래스라는건 ‘클래스이름을 함수처럼 호출해서 객체를 생성한 뒤에, 객체에 .을 찍어서 메소드, 즉 객체에 속한 함수를 불러서 사용하면 되는구나. 객체를 생성할 때 어떤 인자를 줘야하는지는 __init__ 함수(메소드)를 보면 되는구나.’

예제 코드를 따라 작성하시다가, ‘어? 이건 변수인데 그 뒤에 . 을 찍어서 함수를 호출하네? 아, 이건 어떤 클래스의 객체인가보다’를 알아보실 수 있을겁니다.

파일 다루기

이번에는 파일을 읽고 쓰는 방법을 알아봅시다.

fin = open('assets/day1-example-read.txt')
content = fin.read()
print(content, end='')
fin.close()
ID,Sex,Age,Programming?,Python?
1,M,25,T,T
2,F,27,T,F
3,F,24,F,F
4,F,25,T,T
5,M,32,F,F
6,M,39,T,F

위의 코드를 살펴봅시다. open() 이라는 함수가 있고, 그 인자로 파일 경로를 전달합니다. 그러면 open() 함수는 파일 객체를 반환합니다. 파일 객체를 fin 이라는 이름의 변수에 담았고, 파일 객체의 메소드 중 read() 라는 메소드를 사용하여 파일의 내용을 모두 읽어들여 content 변수에 저장합니다. 파일을 모두 사용한 후에는 반드시 close() 메소드를 호출하여 파일을 닫아주어야 합니다.

위 예제는 아래와 같이 줄여서 쓸 수 있습니다.

with open('assets/day1-example-read.txt') as fin:
      content = fin.read()
print(content, end='')
ID,Sex,Age,Programming?,Python?
1,M,25,T,T
2,F,27,T,F
3,F,24,F,F
4,F,25,T,T
5,M,32,F,F
6,M,39,T,F

여기서 with A as B 구문이 등장하는데, with 구문은 자신의 범위를 벗어난 경우 적절한 리소스 반환 작업을 수행합니다. (모든 리소스에 with 구문을 사용할 수 있는 것은 아닙니다.)

파일이 작을 때는 위와 같은 방식으로 읽어들여도 좋지만, 파일이 매우 커서 메모리에 담을 수 없는 경우에는 한줄씩 읽어들여 처리하는 경우가 일반적입니다. 텍스트 파일을 열어 한줄씩 읽어들이는 구문은 아래와 같습니다. (아래 코드를 실행하기 전에, 먼저 파이썬 소스 파일과 같은 디렉토리에 assets 디렉토리를 만들고, 텍스트 파일assets 디렉토리 안에 저장해야 합니다.)

fin = open('assets/day1-example-read.txt')
for line in fin:
   print(line, end='')
fin.close()
ID,Sex,Age,Programming?,Python?
1,M,25,T,T
2,F,27,T,F
3,F,24,F,F
4,F,25,T,T
5,M,32,F,F
6,M,39,T,F

open 함수에 대해 조금 더 살펴봅시다.

위의 예에서는 파일을 읽는 경우를 다루었는데, 파일에 기록하는 경우는 어떻게 할까요? 그런 경우에는 mode 인자에 write라는 의미로 'w' 값을 주면 됩니다. open() 함수의 도움말을 읽어보면, mode 의 기본값이 'r', 즉 read라는 것을 알 수 있습니다.

with open('outputs/myoutput.txt', 'w') as fout:
    fout.write('안녕 텍스트')

잠깐, 한글을 출력하면서 어떤 인코딩 방식을 사용하라고 따로 지정하지 않았는데, 그러면 어떤 인코딩을 사용한걸까요? Spyder 편집기로 파일을 열어보면 인코딩을 알 수 있습니다. 기본 인코딩은 사용하는 OS마다 다른데, Windows에서는 아마도 EUC-KR 로 저장되었을 것이고, MacOS 등 기타 OS에서는 아마 UTF-8 로 저장되었을 것입니다. 확실히 해두기 위해서는 한글을 출력할 때는 항상 encoding 인자를 명시해주면 좋겠네요.

with open('outputs/myoutput.txt', 'w', encoding='utf8') as fout:
    fout.write('안녕 텍스트')

읽을 때 역시 encoding 인자를 줄 수 있습니다. EUC-KR 인코딩으로 기록된 파일을 한번 읽어들여봅시다.

with open('assets/moon_speech_euckr.txt', encoding='euckr') as fin:
    print(fin.readline())
존경하고 사랑하는 국민 여러분. 감사합니다. 국민 여러분의 위대한 선택에 머리 숙여 깊이 감사드립니다. 저는 오늘 대한민국 제 19대 대통령으로서 새로운 대한민국을 향해 첫걸음을 내딛습니다. 지금 제 두 어깨는 국민 여러분으로부터 부여받은 막중한 소명감으로 무겁습니다. 지금 제 가슴은 한번도 경험하지 못한 나라를 만들겠다는 열정으로 뜨겁습니다. 그리고 지금 제 머리는 통합과 공존의 새로운 세상을 열어갈 청사진으로 가득 차 있습니다.

open() 함수를 실행한 결과로는 File object 가 반환됩니다. 위의 예에서 fin, fout 에 담겨 있는 것은 모두 동일하게 File object 입니다. File object 의 메소드 중 자주 사용하는 것들은 아래와 같습니다:

  • read()
  • readline()
  • write(str)

데이터를 다루다보면, CSV 형식의 데이터 파일을 접할 경우가 많습니다. 특히 Excel 프로그램에서 ‘다른 이름으로 저장’을 통해 생성한 CSV 파일은, 값에 구분자인 , 가 포함되어 있는 경우, 값을 따옴표(“)로 감싸줍니다. 이런 경우에는 csv 모듈을 사용하면 편리합니다.

import os
import csv

with open(os.path.join('outputs', 'basic-1-csv-writer.txt'), 'w', encoding='utf8') as fout:
    writer = csv.writer(fout)
    writer.writerow(['안녕 텍스트', 'https://www.wikipedia.org'])
    writer.writerow(['안녕,파이썬', 'https://python.org'])
안녕 텍스트,https://www.wikipedia.org
"안녕,파이썬",https://python.org
import os
import csv

with open(os.path.join('outputs', 'basic-1-csv-writer.txt'), encoding='utf8') as fin:
    reader = csv.reader(fin)
    for row in reader:
        print(row)
['안녕 텍스트', 'https://www.wikipedia.org']
['안녕,파이썬', 'https://python.org']

위에서 os.path.join() 함수를 사용했는데, 이 함수는 디렉토리 이름을 OS에 맞게 만들어주는 함수입니다. OS마다 디렉토리를 지정하는 방식에 약간씩 차이가 있습니다. 주로는 Windows OS와 Unix 계열의 OS(MacOS도 여기에 포함됩니다)가 서로 크게 차이납니다:

  • Windows: C:\Users\toracle\anaconda3
  • MacOS: /Users/toracle/anaconda3

위에서 보듯이, 사소한 차이이긴 하지만, 윈도우에서는 디렉토리 구분자로 \를, MacOS에서는 /를 사용하는 것을 알 수 있습니다. os.path.join() 함수를 사용해서 디렉토리를 만들면 실행되는 OS에 적합한 디렉토리 이름을 반환합니다.

import os
os.path.join('outputs', 'basic-1-csv-writer.txt')

위 함수는, 각각 아래와 같은 경로명을 반환합니다:

  • Windows: outputs\basic-1-csv-writer.txt
  • MacOS: outputs/basic-1-csv-writer.txt

하지만 Python 코드상에서 / 를 디렉토리 구분자로 공통적으로 사용해도, 많은 경우 Python이 OS에 알맞게 적절하게 변환해주기는 합니다.

연습문제

함수를 잘 만들어 사용하는 것은 복잡한 프로그램을 작성해갈수록 매우 중요한 능력입니다.

assets 디렉토리에는 아래와 같은 대통령 취임사 연설문이 준비되어 있습니다.

  • 19대 문재인 대통령 취임사: moon_speech.txt
  • 18대 박근혜 대통령 취임사: park_speech.txt
  • 17대 이명박 대통령 취임사: mb_speech.txt
  • 16대 노무현 대통령 취임사: mh_speech.txt
  • 15대 김대중 대통령 취임사: dj_speech.txt

각 취임사에 대해, 가장 많이 사용된 7개의 어절(단어)이 무엇인지, 그 횟수는 얼마나 되는지 출력해보세요. 아, 1글자짜리 어절은 제외할까요?

참고로 dict 로부터 가장 많은 빈도를 추출하는 코드는 아래와 같습니다:

a_dict = {'a': 10, 'b': 5, 'c': 30, 'd': 24, 'e': 9}
sorted_list = sorted(a_dict.items(), key=lambda x: x[1], reverse=True)[:3]
print(sorted_list)
[('c', 30), ('d', 24), ('a', 10)]

sorted 함수는, 주어진 리스트나 튜플을 정렬합니다. 리스트나 튜플의 원소가 하나의 상수값이 아니라 리스트나 튜플이 중첩되어 있는 경우에는 어떤 값을 기준으로 정렬해야 할지 모르기 때문에, key 인자를 사용해서 어떤 원소를 정렬에 사용해야 하는지 지정해줍니다. 그리고 sorted 함수는 기본적으로 오름차순 정렬을 하는데, reverse 인자를 사용해서 내림차순으로 정렬하게 할 수 있습니다.

정렬을 한 후 많이 함께 사용되는 연산이, ‘가장 많은 순서로 3개’, ‘가장 적은 순서로 5개’와 같이 정렬된 결과에 대해 slicing하는 것입니다. 위 코드에서도 내림차순으로 정렬한 후에 3번째 원소까지만 출력하도록 slicing해주었습니다.

우선 의사 코드(psudo code)로 살펴볼까요?

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

# 파일로부터 내용을 읽어들인다
# 한 행씩 하나의 리스트가 되도록 나눈다(split)
# 단어별 빈도를 저장할 빈 dict를 만든다.
# 한 행씩 순회하면서 수행한다 (for)
#   해당 행에서 부호(, . ! 등)를 없앤다 (replace)
#   빈 칸을 기준으로 어절 단위로 분리하여 리스트로 만든다 (split)
#   단어 리스트를 순회하면서 수행한다 (for)
#     먄악 단어의 길이가 2보다 작다면 해당 순회를 건너뛴다 (if, continue)
#     단어 빈도 dict에, 해당 단어의 빈도를 하나 증가시킨다
# 빈도순으로 내림차순 정렬하고, 상위 7개를 잘라서 반환한다 (sorted, list slicing)

이제 의사 코드에 따라서 하나씩 Python 코드로 표현해보겠습니다.

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

path = 'assets/moon_speech.txt'
text = ''
n = 7

# 파일로부터 내용을 읽어들인다
with open(path) as fin:
    text = fin.read()

# 한 행씩 하나의 리스트가 되도록 나눈다(split)
lines = text.split('\n')

# 단어별 빈도를 저장할 빈 dict를 만든다.
word_counts = {}

# 한 행씩 순회하면서 수행한다 (for)
for line in lines:
    # 해당 행에서 부호(, . ! 등)를 없앤다 (replace)
    striped_line = line.strip().replace(',', '').replace('.', '').replace('!', '')

    # 빈 칸을 기준으로 어절 단위로 분리하여 리스트로 만든다 (split)
    words = striped_line.split()

    # 단어 리스트를 순회하면서 수행한다 (for)
    for word in words:
        # 만약 단어의 길이가 2보다 작다면 해당 순회를 건너뛴다 (if, continue)
        if len(word) < 2:
            continue
        # 단어 빈도 dict에, 해당 단어의 빈도를 하나 증가시킨다
        word_counts[word] = word_counts.get(word, 0) + 1

# 빈도순으로 내림차순 정렬하고, 상위 7개를 잘라서 반환한다 (sorted, list slicing)
result = sorted(word_counts.items(), key=lambda x: x[1], reverse=True)[:n]

print(result)

이것을 함수로 나타내보겠습니다.

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

def count_words(path, n=7):
    # 파일로부터 내용을 읽어들인다
    with open(path) as fin:
        text = fin.read()

    # 한 행씩 하나의 리스트가 되도록 나눈다(split)
    lines = text.split('\n')

    # 단어별 빈도를 저장할 빈 dict를 만든다.
    word_counts = {}

    # 한 행씩 순회하면서 수행한다 (for)
    for line in lines:
        # 해당 행에서 부호(, . ! 등)를 없앤다 (replace)
        striped_line = line.strip().replace(',', '').replace('.', '').replace('!', '')

        # 빈 칸을 기준으로 어절 단위로 분리하여 리스트로 만든다 (split)
        words = striped_line.split()

        # 단어 리스트를 순회하면서 수행한다 (for)
        for word in words:
            # 만약 단어의 길이가 2보다 작다면 해당 순회를 건너뛴다 (if, continue)
            if len(word) < 2:
                continue
            # 단어 빈도 dict에, 해당 단어의 빈도를 하나 증가시킨다
            word_counts[word] = word_counts.get(word, 0) + 1

    # 빈도순으로 내림차순 정렬하고, 상위 7개를 잘라서 반환한다 (sorted, list slicing)
    result = sorted(word_counts.items(), key=lambda x: x[1], reverse=True)[:n]
    return result

print(count_words('assets/moon_speech.txt'))
[('대통령이', 16), ('국민', 12), ('되겠습니다', 12), ('새로운', 7), ('것입니다', 6), ('대통령', 6), ('여러분', 5)]