파이썬의 파일입출력 정리

프로그램에서 처리한 결과는 화면에 출력되기도 하지만, 나중에 따로 사용하기 위해서 별도의 저장 공간에 기록해 둘 필요도 있을 수 있다. 별도의 공간에 기록해두고 다시 쓴다는 것은 곧 파일의 형태로 디스크에 기록하고, 다시 파일의 내용을 읽어들일 수 있어야 한다는 것이다. 이처럼 파일에 어떤 데이터를 기록하고 다시 파일을 읽어들이는 것은 초보자가 생각하기에는 어려운 과정일 수도 있지만, 사실 “입출력”이라는 프로그램의 기본적인 기능에 관련된 부분이기 때문에 대부분의 프로그래밍 언어가 이에 대한 기본적인 API를 제공해주고 있다.

파일을 보는 시각에 대해

파일 입출력에서 API 사용법보다 중요한 것은 파일에 데이터를 어떤 식으로 기록하고 읽을 것인가에 대한 것이다. 보통 프로그래밍 언어 자습서에서는 파일의 종류를 두 가지로 구분하여 소개한다. 하나는 이진 데이터를 기록하는 바이너리 파일이고 다른 하나는 “일반 텍스트”를 저장하는 텍스트파일이다. 하지만 “일반 텍스트”라는 표현은 사실 형용모순인데, 왜냐하면 “일반 텍스트 파일”이라는 것은 존재하지 않기 때문이다! 파이썬을 비롯한 모든 프로그래밍 언어에서 컴퓨터는 사실 문자를 인식할 수 없고 0과 1로 된 숫자값만 인식할 수 있다. 그래서 이진수로 표현할 수 있는 데이터는 정수 혹은 실수의 수학적 숫자값이다. 컴퓨터에서 글자라는 것은 어떤 글자의 표를 정해놓고 그 표에서 “몇 번째에 있는 글자”라는 식으로 맵핑하여 인식한다. 따라서 본질적으로 텍스트 정보는 일련의 숫자값 정보이다.

현대 프로그래밍 언어에서 이러한 텍스트를 위한 만들어진 거대한 코드표를 유니코드라 한다. 파이썬3에서 사용하는 문자열 규격역시 유니코드 문자열이다. 그리고 유니코드 문자열은 파일에 기록하거나 네트워크를 통해 전송하는 등의 처리를 하기 전에 일련의 이진데이터로 인코딩되어야 한다. 우리가 UTF-8이나 EUC-KR이라고 하는 인코딩은 이럴 때 사용된다. 인코딩된 문자열 정보는 결국 바이너리 데이터이기 때문에 프로그래밍 언어가 읽고 쓰는 유일한 데이터 포맷은 이진파일 밖에 없다고 봐야 한다.

따라서 파이썬에서 일반적으로 데이터 파일을 읽고 쓰는 방법은 다음의 절차를 거친다고 보면 된다.

  1. 인코딩된 데이터와 문자열을 상호 변환하기 위해서는 어떤 인코딩을 사용할 것인지를 결정해야 한다. (보통 이것은 UTF8을 쓴다고 생각하면 된다.)
  2. 모든 데이터 파일은 결국 이진데이터 파일이며, 이진 데이터는 파이썬 내에서 bytesbytearray로 취급된다.
  3. 문자열을 이진데이터 파일에 기록하기 위해서는 문자열을 bytes 타입으로 변환한 후에(이것이 인코딩이다.) 그대로 파일에 쓰면 된다. 하지만 이 과정은 파이썬 내부에서 이루어진다.
  4. 이진포맷으로 기록된 텍스트는 bytes로 읽어들여지며, 다시 인코딩을 사용해서 이를 문자열로 변환된다. 이것이 텍스트 파일을 읽는 절차이다.

파이썬3에서는 파일을 열 때, 포맷이 텍스트 모드라면 3, 4의 과정을 내부적으로 자동으로 처리한다. 따라서 문자열을 그대로 읽고 쓰는 것처럼 다룰 수 있다.

사실 이 부분이 파이썬 2보다는 3를 써야하는 가장 큰 이유이기도 하다. 파이썬 3에서는 문자열과 바이트 배열이 명확하게 구분되며, 명시적인 인코딩/디코딩을 거쳐 서로의 타입으로 변환된다. 하지만 파이썬 2에서는 그런 거 없었고, 입출력 과정에서 글자가 깨지는 증상이 생기면 프로그래머만 죽어나는 상황이 된다.

자, 그러면 데이터 파일에 무엇을 쓰고 읽게 되는지 알게 되었으니, 실제로 어떻게 하는지를 알아보자.

open

파일을 읽고 쓰기 위해서는 먼저 파일을 “열어야”한다. 파일을 여는 행위는 기본함수인 open()을 사용할 수 있다. 이 함수에서 사용하는 파라미터와 리턴값은 다음과 같다.

open(파일경로와이름, 모드, 인코딩) -> 파일 디스크립터
  • 먼저 어떤 파일에 쓰거나 파일을 읽을 것이냐에 대해서 파일 이름을 결정해주어야 한다. 기본적으로 문자열로 된 파일의 이름을 주면 된다.
  • 모드는 파일을 가지고 어떤 일을 할 것인지를 결정한다.
  • 텍스트 파일을 쓰거나 읽기 위해서는 인코딩을 지정해야 한다.

open() 함수를 이용해서 파일을 열게 되면 우리는 파일 디스크립터(file descriptor)라는 것을 얻게 된다. 파일 디스크립터는 파일의 내용을 액세스하기 위한 일종의 핸들 같은 것이다. 디스크립터가 제공하는 다음의 메소드를 사용할 수 있다.

  • file.write(데이터) : 주어진 데이터를 파일에 쓴다. 이 동작을 하기 위해서는 파일을 쓰기 모드로 열어야 한다.
  • file.read(바이트) : 주어진 바이트 수 만큼 파일의 내용을 읽어들인다. 바이트 수를 주지 않으면 파일의 끝까지 읽게 된다.
  • file.close() : 파일을 닫는다.

파일의 모드는 데이터의 종류와 액션의 종류가 된다.

플래그의미비고
r읽기모드

w쓰기모드파일을 쓰기 모드로 연다. 파일에 데이터를 쓰면 기존 파일의 내용은 모두 날아간다. 주어진 파일이 존재하지 않으면 새로운 파일을 만든다.
x쓰기 전용새 파일 쓰기 모드로 연다. 주어진 이름의 파일이 존재하면 에러가 발생한다.
a추가모드파일을 추가 모드로 연다. 기존 파일의 내용의 끝에 새 내용을 추가하여 기록한다.
+갱신모드파일을 읽기와 쓰기가 모두 가능한 모드로 연다.
rb이진 읽기 모드이진 파일을 읽기 모드로 연다. 읽어들인 데이터는 디코딩되지 않은 바이트배열이 된다.
wb이진 쓰기 모드이진 파일을 쓰기 모드로 연다. 쓰는 데이터는 raw 데이터 그대로 쓰인다.

기본적인 텍스트 파일 읽기

기본적인 텍스트 파일 읽기 동작을 수행하는 코드를 보자. 다음의 예제는 주어진 텍스트 파일을 읽어서 그 내용을 출력하는 프로그램이다.

import sys   ## 1)

if len(sys.argv) > 1: ## 2)
    filename = sys.argv[1]
    file = open(filename, 'r', encoding="utf8")  ## 3)
    text_str = file.read()  ## 4)
    print(text_str)  
    file.close()  ## 5)
  1.  스크립트를 실행할 때 인자를 받기 위해서 sys 모듈이 필요하다.
  2. 실행 인자는 sys.argv에 저장되어 있다. 이 리스트의 첫 인자는 스크립트 파일 이름이며, 그 이후부터가 전달된 인자이다.
  3. open() 함수를 이용해서 파일을 텍스트 읽기 모드로 열었다. 이 때 파일의 인코딩은 utf8이라고 본다.
  4. 파일의 전체 내용을 읽어서 text_str 에 할당한다. 이 값은 문자열 타입이다.
  5. 파일의 내용을 출력한 후에 파일을 닫아준다.

기본적인 텍스트 파일 쓰기

이번에는 텍스트 파일을 쓰는 방법에 대해서 살펴보자. 간단하게 구구단 7단을 출력하는 프로그램을, 파일로 저장하는 방식으로 수정해가는 방식으로 살펴보겠다. 먼저 구구단 7단은 다음과 같이 출력할 수 있다.

for a in range(1, 9):
  line = f'7 * {a} = {a * 7}'
  print(line)

이 코드를 구구단 7단을 파일로 저장하는 과정을 살펴보자. 파일을 읽는 것과 거의 비슷하다. 다만 file.read() 대신에 file.write()를 써서 파일에 데이터를 쓴다는 것만 차이가 있다.

file = open('seven.txt', 'w', encoding='utf8')
for a in range(1, 9):
  line = f'7 * {a} = {a * 7}\n'  ## 1)
  file.write(line)
file.close()

다만 주의할 것은 각 라인의 뒤에는 “줄바꿈 문자”가 들어가야 한다는 점이다. 줄 바꿈 문자가 없는 텍스트는 “일련의 단어”가 되기 때문에 모든 내용이 한줄로 출력될 것이다. 각 라인을 한 번씩 쓰는 방법도 있지만, 다음과 같이 모든 라인을 하나의 텍스트로 join 한 후에 한 번만 기록하는 방법도 있다.

lines = '\n'.join(f'7 * {a} = {a * 7}' for a in range(1, 10))
file = open('seven.txt', 'w', encoding='utf8')
file.write(lines)
file.close()

컨텍스트 매니저

파일을 사용하기 위해 열었다가 다시 닫는 일이 번거로울 수 있는데, 파일 디스크립터는 “컨텍스트 매니저”의 일종으로 정의되어 있다. 컨텍스트 매니저는 파이썬의 with 구문에 사용될 수 있는 객체를 말한다. 파일을 읽어서 출력하던 최초의 코드는 다음과 같이 다시 쓰일 수 있다.

with open('myfile.txt', encoding='utf8') as f:
  contents = file.read()
  print(contents)

이 코드는 open() 함수로 파일을 열고 그 리턴되는 디스크립터를 f 라는 이름에 바인딩한다. 이후 들여쓴 블럭 내에서 f는 파일 핸들로러 기능하게 된다. 코드 블럭에서 명시적으로 f.close()를 호출하지는 않지만, 들여쓰인 코드 블럭의 실행이 끝나서 블럭을 빠져나오는 시점에 자동으로 f.close()가 호출되어 파일이 닫히게 된다. 이 방식은 예외 처리 등에 의해서 파일을 닫아야 하는 코드가 여러 군데에 들어가야 하는 상황에서도 닫힘을 자동으로 처리하게 되기 때문에 깔끔한 처리를 가능하게 한다.

텍스트 파일을 줄 단위로 읽기

텍스트 파일에서 가장 흔히 쓰이는 케이스는 텍스트 파일의 내용을 한 줄씩 읽어들여서 처리하는 것이다.  예를 들어서 어떤 연속된 데이터들에 대해서 하나의 레코드를 하나의 행으로 변환하여 파일에 쓴 경우, 이를 거꾸로 읽어들여서 처리하려면 파일을 줄 단위로 읽는 편이 조금 더 처리가 용이할 수 있다. 텍스트 파일을 줄 단위로 처리하는 방법에는 크게 세 가지가 있을 수 있다.

  1. 파일의 전체 내용을 한 번에 읽어서 개행 문자로 분리하는 것
  2. 파일을 한 번에 한줄씩만 읽는 처리를 계속하는 것
  3. 파일 디스크립터를 for 문에 사용하는 것

먼저 첫번째 방법은 가장 무식한(?) 방법이다. 텍스트 파일은 여러 줄로 구분된 데이터이므로, 데이터를 file.read()와 같이 한번에 전체를 다 읽어들인 다음에 문자열의 .split() 메소드를 사용해서 각 라인별로 끊어서 문자열의 리스트를 만드는 방법이다. 이 방식은 편리할 수는 있지만, 텍스트 파일의 크기가 매우 큰 경우에 모든 콘텐츠를 메모리로 다 읽어들인다는 부분이 문제가 될 수 있다.

with open('seven.txt') as f:
  lines = f.read().split()
  for line in lines:
    print(line)

이 방법을 위한 보다 편리한 메소드로 file.readlines() 가 있다. 모든 라인을 읽어서 각 라인 텍스트의 리스트를 리턴하는 함수이다.

두 번째 방법은 한 번에 필요한 만큼 한 줄씩 읽어들여서 처리하는 방법이다.  텍스트 파일의 크기가 한정되지 않았다면 비교적 안전하고 또 널리 쓰이는 방법이다.

with open('seven.txt') as f:
  while True:   
    line = f.readline()
    if not line:
      break
    print(line)
    

세 번째 방법은 조금 특별한데, 가장 권장되는 방법이다. 기본적으로 텍스트 읽기 모드로 열린 파일은 매번 각 라인을 읽어서 리턴하는 방법을 스스로가 알고 있다. 이러한 동작은 파일의 끝까지 읽어들였을 때까지 반복될 수 있는데, 이 성질은 마치 반복자나 제너레이터와 유사하다. 따라서 파일 디스크립터는 그 자체로 각 라인의 텍스트를 만들어내는 제너레이터와 동일하게 취급될 수 있다. 이 말은 for ... in  구문에서 파일 디스크립터를 그대로 써도 무방하다는 것이다.

with open('seven.txt') as f:
  for line in f:
    print(line)

파일 디스크립터는 느긋한 제너레이터이므로 위 코드는 아무리 큰 텍스트 파일을 열었다하더라도 한 라인의 크기가 적절한 수준이라면 안전한 방법인 동시에 코드 역시 깔끔하게 처리된다. 다음 코드는 이 방법을 응용한 것으로 한 파일의 각 라인을 읽어들여서 대문자로 변환하여 출력하는 방법이다.

with open('sometext.txt', encoding='utf8') as f:
  for line in (x.upper() for x in f):
    print(line)

텍스트 파일 복사하기

텍스트 파일을 읽어서 한줄씩 출력하고 다시 이를 다른 파일에 쓰는 프로그램을 작성해보자. 하나의 함수로 작성할 것이다. 이는 위에서 배운 몇 가지 사실만 기억한다면 매우 간단하게 작성될 수 있다.

  • 원본 파일과 대상 파일을 각각 읽기 모드와 쓰기 모드로 열어야 한다. 따라서 프로그램 전체 코드는 2개의 with 문이 중첩된 모양이 될 것이다.
  • 읽어들인 각 라인을 출력한 후에 대상 파일에 기록하면 된다. 행 단위로 처리되기 때문에 기록할 때에는 개행문자를 붙여주면 된다.
def copy_and_print(filename):
  with open(filename, encoding="utf8") as f1:
    targetfile = 'copied_' + filename
    with open(targetfile, 'w', encoding='utf8') as f2:
      for line in f1:
        print(line)
        f2.write(line + '\n')

이진 파일 복사하기

이진 파일을 복사하는 방법도 간단하다. 파일을 이진 모드로 두 개 열고 한쪽에서 읽을 데이터를 다른쪽으로 쓰면 된다. 단, 여기서 주의해야 할 점은 read()를 통해서 한 번에 모든 내용을 다 읽어들이는 것은 좀 위험하다는 것이다. 텍스트파일보다 훨씬 더 높은 확률로 이진 파일은 가용 메모리 보다 클 수 있다는 점이다.

.read(size) 메소드는 파일의 데이터를 주어진 사이즈만큼의 바이트 수를 읽어들인다. 파일을 읽어들이다가 파일의 끝을 만나면 파일의 끝까지만 읽는다. 이렇게 읽어들인 이진데이터는 아무런 변환없이 그대로 대상파일에 기록한다. 버퍼로 읽어들인 데이터가 없다면 모든 처리가 완료된 셈이다.

def copy_bin_file(filename):
  targetfile = 'copied_' + filename
  chunk_size = 1024 * 4096 ## 4MB 단위로 읽는다.
  with open(filename, 'rb') as f1:
    with open(targetfile, 'wb') as f2:
      while True:
        ## 최대 청크 사이즈까지 데이터를 읽은 후 이를 대상 파일에 쓴다.
        ## 이 과정을 더 이상 읽을 데이터가 없을 때까지 반복한다.
        buffer = f1.read(chunk_size)
        if not buffer:
          return
        f2.write(buffer)

간단한 합계, 평균 프로그램

학생의 이름, 국어, 수학, 영어 점수를 입력받아서 각 학생의 총점과 평균, 그리고 전체 학생의 수학 점수의 평균을 출력하는 프로그램을 작성한다고 생각해보자. 물론 키보드를 이용해서 각 데이터를 입력받는 식으로 프로그램을 작성할 수도 있다. 하지만 그렇게 일일이 타이핑하는 것은 너무나 수고스러운 일이니 다음과 같이 텍스트 파일에 개별 학생의 성적이 기입된 데이터가 있다고 가정하자.

홍길동 87 90 82
최점례 91 80 87
오서방 73 92 99
...

그렇다면 우리는 다음과 같은 방식으로 데이터를 처리할 수 있을 것이라 생각해볼 수 있다.

  1. 각 라인의 데이터는 모두 문자열이며, 이름, 국어, 수학, 영어 점수 순으로 나뉜다. (이름에는 띄어쓰기가 없다고 가정한다.)
  2. 각 라인의 데이터를 하나의 사전으로 변환할 수 있다.
  3. 이 말은 전체 데이터는 사전의 리스트로 변환될 수 있다는 것이다.
  4. 리스트의 각각의 사전에 대해서 세 개의 점수 값을 합계, 평균 내는 것은 간단한 일이다.
  5. 리스트의 각 사전에 대해서 특정한 과목 키를 이용해서 그 값을 합계, 평균 낼 수도 있을 것이다.

먼저 각 학생의 총점과 평균을 출력하는 함수를 작성해보자.

def print_personal_marks(aDict):
  keys = ('국어', '수학', '영어')
  total = sum(aDict[x] for x in keys)
  avg = total / 3
  print(f'{이름: aDict['이름'] - 총점: {total}, 평균: {avg:.2f}')

다음은 정수의 리스트를 받아서 총점과 합계를 출력하는 함수이다.

def print_class_marks(marks):
  total = sum(marks)
  avg = total / len(marks)
  print(f'총점: {total}, 평균: {avg:0.2f}')

자 이제 파일을 읽어서 성적 정보를 파싱하는 코드를 생각해보자. 한 줄의 정보를 입력받은 것은 다음과 같이 처리할 수 있다.

aLine = ....
keys = ('국어', '수학', '영어')
name, *marks = aLine.split(' ')  # 공백으로 분리한 첫 값은 name, 그외 값은 marks라는 리스트로
## marks의 각 요소를 정수로 변환하고 키와 조합하여 사전으로 만든다.
anItem = dict(zip(keys, (int(x) for x in marks)))
## 여기에 이름을 추가하여 주고 데이터에 추가한다.
anItem['이름'] = name

자 위 코드는 “한줄의 데이터” ==> “하나의 사전객체”로 변환하는 과정을 표현하고 있다. 따라서 “여러 줄의 데이터”가 있다면 이 동작을 맵핑하여 “모든 사전의 리스트”로 변환할 수 있을 것이다. 개별 데이터를 변환하고 출력하는 부분을 모두 함수로 만들면 여러줄의 리스트는 간단히 성적의 데이터 리스트로 변환되고, 각각 학생의 성적과 전체 수학 성적은 매우 간단하게 출력할 수 있다.

### 위의 코드를 함수로 정의하고 읽어들인 데이터로 성적 데이터를 만든다.
def process_line(line):
  keys = ('국어', '영어', '수학')
  name, *marks = line.split()
  anItem = dict(zip(keys, (int(x) for x in marks)))
  anItem['이름'] = name
  return anItem

with open('data.txt', encoding='utf8') as f:
  data = [process_line(line) for line in f]
  ## 각 학생의 성적을 출력한다.
  for x in data:
    print_personal_marks(x)

  ## 전체 수학 성적을 출력한다. 
  print("전체 수학 성적")
  print_class_marks([item['수학'] for item in data])

정리

파일 입출력과 관계된 내용에 대해 많은 것들을 살펴보았는데, 대략 다음과 같이 정리할 수 있겠다.

  1. 파일은 항상 이진 데이터로 저장되지만, 실질적으로 많이 쓰이는 입출력은 텍스트파일에 관한 것이다. 따라서 텍스트 파일을 읽고 쓸 때에는 인코딩 정보를 항상 알고 있어야 한다.
  2. open()으로 파일을 열 수 있다. 가능하면 with 문과 함께 쓰는 편이 편리하다.
  3. 파일은 한 번에 전부 읽거나 한 번에 한줄씩 읽을 수 있다. 한 번에 한 줄씩 읽는 경우라면 파일 디스크립터를 파일 각 라인의 제너레이터라고 간주할 수 있다.

실제로 개념을 보이기 위한 예제를 제외하면 파일을 읽고 쓰는 코드는 매우 사소한 부분을 차지한다는 것을 알 수 있다. 파이썬은 입출력을 단순화하는 쪽으로 발전해왔고, 실제로 마지막 예를 보면 파일을 읽고 쓰는 것보다는 데이터를 다루는 부분에 집중하는 식으로 문제를 해결하는 것을 볼 수 있다. 늘 하는 이야기지만, 가급적 어떤 코드를 작성할 때 문제를 한 번에 해결하려고 하기보다는 개별 데이터를 다루는 코드를 함수로 작성하고, 데이터의 리스트 (혹은 연속열)을 다루는 식으로 이를 확장해 나가도록 하자. 그러면 그외의 입출력과 관련된 부분은 이러한 리스트와 함수를 조합하는 과정의 코드들과 아주 자연스럽게 어울리는 식으로 연결될 수 있다.

[Python101] 004. 파일. 파일을 읽고, 파일에 쓰기

사용자로부터 입력을 받아 데이터를 처리하는 프로그램은 실질적으로 효율에 한계가 있다. 처리해야 하는 데이터를 사용자가 일일이 매번 입력해야 하기 때문이다. 컴퓨터는 귀찮고 반복적으로 처리하는 작업을 수월하게 하라고 있는 것이므로 프로그램은 가능한한 많은 과정을 자동화하는 방향으로 처리하는 것이 좋은 경우가 많다.

보통의 경우에는 다음과 같은 방식의 순서로  작업을 많이 처리하게 된다.

  1. 처리해야 할 데이터를 미리 준비 (텍스트 파일이나 엑셀 파일 등)
  2. 프로그램을 실행할 때 매개변수로 처리할 파일을 주고 실행한다.
  3. 프로그램이 데이터 파일을 읽어 들여서 데이터를 주르르르륵 처리한다.
  4. 처리한 결과는 또 다른 파일에 기록되거나 화면에 출력된다.

이것은 일종의 자동화 작업이고, 파이썬은 이런 작업에 대한 처리를 쉽고 빠르게 처리하기에 매우 좋다.

파일 읽기

데이터를 저장한 파일로부터 데이터를 읽는 방법에 대해 먼저 살펴보도록 하자. 예를 들어 메모장으로 작성한 텍스트 파일의 내용을 읽어들이고, 뭔가 내용을 쓰는 작업은 다음과 같은 절차를 거친다.

  1. 파일을 연다. 파일을 열게 되면 파이썬은 이 파일을 다루기 위한 핸들러(Handler)를 사용하게 된다.
  2. 파일의 내용을 읽어들인다. 파일의 내용을 읽어들이는 작업은 파일을 열면서 생성한 핸들러가 중계해준다. 핸들러는 파이썬 스크립트와 실제 데이터 파일 사이를 중계해주는 모듈이라 생각하면 된다.
  3. 파일에는 새로운 내용을 쓰거나, 기존 파일의 내용 뒤에 내용을 추가하는 것도 가능하다. 이 역시 핸들러에 의해 데이터를 쓸 수 있다.
  4. 파일 다 사용했으면 파일을 닫는다. 만약 파일을 적절히 닫지 않으면 파이썬 프로세스가 정상적으로 종료되지 못하거나, 다른 프로그램이 해당 파일을 쓰지 못하는 경우가 발생할 수 있다.

파일을 열기 위해서는 open 명령을 사용한다. open 명령은 다음과 같이 사용한다.

파일핸들러 = open('파일의 경로', [모드])

open  명령을 사용해서 파일을 열면 파일의 핸들러를 얻을 수 있다. 이 핸들러를 통해 파일의 내용을 읽어들이거나, 파일에 데이터를 쓰는 작업이 가능해진다.

ex09.py

아홉번째 예제는 텍스트 파일을 열어서 그 내용을 보는 예제이다.이 예제를 제대로 실행하기 위해서는 텍스트 파일 하나가 필요하다. 메모장을 열어 아무 텍스트나 몇 줄 입력한 후에 예제를 저장하는 폴더에 저장한다. 이름은 구분하기 쉽게 ex09.txt로 하겠다.

April

Still an irritating wind;
Vestiges of stubborn grey –
Jibing us of recent winter blight.

It’s coming though – like perky breasts
Pushing through a blouse –
Teasing, pleasing in it’s tantalising play:

Warmth of youth in April sun –
Simmering off depression,
Brains retuned; remapped for fun.

April is a portal –
Smoothly transitions
Delicate dispositions – suchlike mine,

Easing hunched bodies into
Summery smiles.

이제 ex09.py 의 소스 코드는 다음과 같다.

from sys import argv

script, filename = argv

txt = open(filename)

    print "Here's your file %r:" % filename
print txt.read()

    print "Type the filename again:"
    file_again = raw_input('> ')

txt_again = open(file_again)
    print txt_again.read()

argv 가 등장하는 것을 보니, 파라미터를 받고 실행하는 프로그램이다. 실행은 앞의 강좌에서 소개했던 것과 같은 방식으로 하면 된다.

 > python ex09.py ex09.txt 이렇게 실행한 결과는 다음과 같다.

Here's your file 'ex09.txt':
April

Still an irritating wind;
Vestiges of stubborn grey
Jibing us of recent winter blight.

It’s coming though  like perky breasts
Pushing through a blouse
Teasing, pleasing in it’s tantalising play:

Warmth of youth in April sun
Simmering off depression,
Brains retuned; remapped for fun.

April is a portal
Smoothly transitions
Delicate dispositions ? suchlike mine,

Easing hunched bodies into
Summery smiles.
Type the filename again:
> ex09.py
from sys import argv

script, filename = argv

txt = open(filename)

print "Here's your file %r:" % filename
print txt.read()

print "Type the filename again:"
file_again = raw_input('> ')

txt_again = open(file_again)
print txt_again.read()

자 이제 코드를 좀 살펴보자.

1) 3행을 보면 파일 이름을 argv로부터 읽어온다.

2) 5행에서는 이 파일 이름을 사용하여 해당 파일을 연다. 파일 이름만 전달 받았으므로 파이썬 코드 파일과 같은 폴더 내의 파일을 탐색하게 된다. 그리고 파일을 열어서 그 핸들러는 txt라는 이름의 변수에 대입된다.

3) txt는 곧 파일 핸들러라고 했다. 파일 핸들러는 여러 기능 모듈을 가지고 있는데 그 중 하나가 read() 이다. 이 모듈은 파일의 내용을 전부 읽어들이는 일을 한다.

이 때 txt 모듈의 read() 를 실행하기 위해서는 구두점(.)으로 이를 연결해준다. 즉 txt.read() 는 txt 모듈의 read라는 기능 모듈을 실행한다는 뜻이다. 실행이 가능한 모듈은 주로 ()가 뒤에 붙는데, 이렇게 실행이되는 모듈을 특별히 ‘함수’ 혹은 ‘메소드’라고 한다.

txt.read() 모듈은 파일 전체의 내용을 읽어 이를 돌려준다. 8행은 이 결과를 모두 출력하게 된다.

4) 10행에서는 다른 파일 이름을 다시 물어본다. 나는 ex09.py를 입력했다. 파이썬 스크립트 소스 자체도 텍스트 파일이다. 단지 관습적으로 .py 라는 확장자를 썼을 뿐이다.

5) 주목할 것은 마지막의 14행이다. txt_again은 파일을 열고 그 핸들러를 대입하게 된다. 즉 txt_again이 파일의 핸들러이고, open() 이 실행된 결과와 동일하다. 즉 open()의 결과가 파일 핸들러라는 이야기다. 따라서 13, 14행은 다음의 한 행으로 합칠 수 있다.

print open(file_again).read()

파일에 쓰기

ex09.py는 파이썬에서 다른 텍스트 파일의 내용을 읽어와서 사용할 수 있음을 보여주었다. 주소록 정리라든지, 성적 계산, 혹은 이름-이메일 정보를 가진 주소록을 가져와서 받는 사람 주소에 맞게 사람 이름이 들어가는 이메일을 보내는 프로그램 등을 만들 수 있을 것이다.

이번에는 내용을 파일에 기록하는 작업에 대해 살펴보자. 지금까지는 open() 명령에서 모드를 따로 지정하지 않았다. 모드를 따로 지정하지 않은 open()은 기본적으로 “텍스트 파일”을 “읽기”모드로 열게 된다. 파일을 기록하기 위해서는 파일을 w 모드로 열면 된다. w 모드로 파일을 여는 것은 사실 새로운 파일을 만들고 그곳에 내용을 기록하는 것을 의미한다. 따라서 파일 이름이 기존에 있던 내용이면, 그 파일을 덮어쓰게(overwrite) 된다는 점을 참고하자.

ex10.py는 사용자로부터 3줄의 텍스트를 입력받아 이를 파일에 기록해주는 프로그램이다.

from sys import argv

script, filename = argv

print "We're going to erase %r." % filename
print "If you don't want that,hit Ctrl-C(^C)."
print "If you do want that, hit RETURN."

raw_input('?')

print "Opening the file..."
target = open(filename, 'w')

    print "Truncating the file. Goodbye!"
target.truncate()

    print "Now I'm going to ask you for three lines."

    line1 = raw_input("Line 1:")
    line2 = raw_input("Line 2:")
    line3 = raw_input("Line 3:")

    print "I'm going to write these to the file."

target.write(line1)
    target.write('\n')
target.write(line2)
    target.write('\n')
target.write(line3)
    target.write('\n')

target.close()

코드를 보면 알겠지만, 텍스트 파일의 이름을 파라미터로 받고 있다. 따라서 콘솔에서 파일을 실행할 때 텍스트 파일의 이름을 준다. 이 코드에는 파일의 내용을 지우는 truncate() 명령이 포함되어 있는데, 위에서 작성했던 ex09.txt 파일을 변경하도록 해보자. 결과는 아래와 같고, 하이라이트 된 라인은 실제 프롬프트에서 명령을 입력한 라인이다.

c:\Apps\textfiles>c:\Python27\python ex10.py ex09.txt
We're going to erase 'ex09.txt'.
If you don't want that,hit Ctrl-C(^C).
If you do want that, hit RETURN.
?
Opening the file...
Truncating the file. Goodbye!
Now I'm going to ask you for three lines.
Line 1:ABCDEFGHIJKLMNOPQRSTUVWXYZ
Line 2:abcdefghijklmnopqrstuvwxyz
Line 3:0123456789
I'm going to write these to the file.

c:\Apps\textfiles>type ex09.txt
ABCDEFGHIJKLMNOPQRSTUVWXYZ
abcdefghijklmnopqrstuvwxyz
0123456789

텍스트 파일의 이름을 파라미터로 주고 프로그램을 실행하면 먼저 기존 파일의 삭제 여부를 묻게 된다. 엔터를 누르면 계속 진행을 하게 되고 파일을 지운다. 그런 다음 세 줄의 텍스트를 입력 받고, 이를 파일에 기록하게 된다.type 명령 (터미널에서는 cat 명령)으로 파일의 내용을 살펴보면 입력한 내용으로 해당 파일의 내용이 바뀌어 있음을 볼 수 있다.

코드의 내용을 분석해보도록 하자.

1) 파일의 이름을 filename 이라는 변수에 대입해준다. 이 파일 이름은 파라미터로 전달받은 값이다.

2) 5~7 행에서는 파일의 이름을 표시하고, 취소할 기회를 준다. 이때 ^C (ctrl+c)를 누르면 프로그램의 실행이 취소되고, 엔터를 누르면 다음으로 진행하게 된다.

3) 11~15행에서 해당 파일을 “쓰기 모드”로 열고 내용을 모두 지운다. 파일핸들러의 truncate() 명령은 내용을 모두 지우는 기능을 수행한다. 사실 “w”를 사용해 쓰기 모드로 파일을 열고 다른 내용을 기록하면 이전의 내용은 모두 지워지게 된다. 따라서 15행은 실질적으로는 필요 없는 셈이다. (15행을 주석 처리하고 한 번 실행해보라.)

4) 17~21행에서 다시 각 라인을 입력 받아 line1, line2, line3에 각각 입력 받은 내용을 추가한다.

5) 25~30 행에서는 파일 핸들러 targetwrite() 기능을 이용해서 각각의 문자열을 파일에 쓴다. 이 때 각 line1, line2, line3 은 줄바꿈 문자를 포함하고 있지 않으므로 이를 추가해 준다. 물론 이는 target.write(line1+"\n") 과 같이 써서 줄바꿈 문자를 더한 문자열을 파일에 써도 된다.

6) 끝으로 파일을 닫는다.

물론 하나의 프로그램에서 파일을 2개 이상 동시에 여는 것도 가능하다. 각각의 열린 파일은 핸들러로 조작할 수 있으므로 2개의 핸들러를 open 명령으로 만들어주면 된다.

파일 복사 프로그램

이 글의 서두에서 언급한 파일 복사하는 프로그램을 작성해 보겠다. 사실은 무척이나 단순하다. 이 프로그램은 1) 파라미터로 원본 파일과 복사할 위치를 입력받는다. 2) 원본 파일의 길이(몇 바이트나 되는지)를 알려주고, 3)복사할 위치에 이미 파일이 존재하는지를 알려준다. 그런 다음 4) 원본 파일의 내용을 읽어와서 사본 파일에 쓴다. 5) 두 파일을 닫는다.

ex11.py

이상의 내용을 코딩한 ex11.py의 내용은 다음과 같다.

#-*-coding:utf-8
from sys import argv
from os.path import exists

script, from_file, to_file = argv

print "Copying from %s to %s" % (from_file, to_file)

    input = open(from_file)
indata = input.read()
    print "The input file is %d bytes long." % len(indata)

    print "Does the output file exists? %r" % exists(to_file)
    print "Reday, hit RETURN to continue, CTRL-C to abort."
raw_input()

    output = open(to_file, 'w')
output.write(indata)

    print "Alright, all done."

    output.close()
input.close()

실행 결과는 다음과 같다. 이미 만들어져있는 그리고 만만한 ex09.txt를 다른 파일로 복사해보도록 하겠다.

c:\Apps\textfiles>c:\Python27\python.exe ex11.py ex09.txt ex09.bak
Copying from ex09.txt to ex09.bak
The input file is 65 bytes long.
Does the output file exists? False
Reday, hit RETURN to continue, CTRL-C to abort.

Alright, all done.

c:\Apps\textfiles>type ex09.bak
ABCDEFGHIJKLMNOPQRSTUVWXYZ
abcdefghijklmnopqrstuvwxyz
0123456789

코드 내용 분석을 보자. 무척이나 단순하고 간단한데, 못 보던 기능을 발견했다. 바로 os.pathexists 라는 명령이다.

1) os.path 로부터 exists라는 함수를 반입해온다. os 는 파이썬 프로그램이 실행되는 컴퓨터의 운영체제와 관련된 모듈들의 집합이다. 그리고 그 중에는 path라는 모듈이 있는데, 이 모듈은 디스크 내의 경로, 파일 등과 관련된 기능들이 모여있다. exists 모듈은 path에 들어있는 하위 객체인데, 주어진 경로에 파일이 이미 존재하는지를 검사하여 존재한다면 True를 돌려주는 기능을 한다.

2) input 은 원본 파일을 열어서 그 핸들러를 얻는다. input의 read() 명령을 통해 파일의 전체 내용을 읽어 indata에 대입한다.

3) 13~15는 파일의 길이를 알려주고, 복사본 위치를 찾아 거기에 이미 파일이 있는지를 알려준다. 엔터를 입력하면 raw_input() 뒤의 코드가 계속 실행된다.

4) 'w' 모드로 복사본 파일을 연다. 그런 다음, indata를 쓴다.

5) 끝으로 두 파일을 닫으면 된다.

위의 실행 결과에서 알수 있듯이, 파라미터로 준 파일을 보면 내용이 복사되어 있음을 알 수 있다.

사실 파일 입출력은 쉬운 내용은 아니다. ‘핸들러’라는 눈에 보이지도 않는 추상적인 개념을 도구로 다루기 때문이다. 하지만 이런 추상적인 개념은 사실 이해하기보다는 익숙해지면 쉽게 느끼게 된다. (우리가 흔히 하는 덧셈, 뺄셈도 분명 그 원리는 우리가 알고 있지만 엄밀하게는 익숙하게 느끼기 때문에 쉽게 생각할 수 있는 것이다.)

파일 입출력에 대해 간단히 알아보았다. 이로써 기본적인 입력과 출력을 처리할 수 있는 방법에 대해서는 기본기를 파악했으니, 다음 시간부터 실제 프로그램을 움직이는 흐름인 로직에 대해 살펴보기로 하겠다.