파일

지속성

지금까지 본 대부분의 프로그램은, 짧은 시간 동안 실행하고 어떤 출력을 만들지만 마지막에는 데이터가 사라진다는 의미에서 일시적입니다. 프로그램을 다시 실행한다면 백지에서 시작합니다.

다른 프로그램들은 지속적(persistent)입니다: 오랜 시간 (또는 늘 계속해서) 실행하고; 적어도 데이터의 일부를 영구적인 기억장소(예를 들어, 하드 드라이브)에 보관하며; 종료 후 다시 시작하면 마지막 지점으로 돌아갑니다.

지속적인 프로그램의 예로는 운영 체제(operating system), 거의 컴퓨터가 켜져 있을 때마다 실행합니다, 와 웹 서버, 네트워크로부터의 요청을 기다리면서 늘 실행 중입니다, 가 있습니다.

프로그램이 데이터를 유지하는 가장 간단한 방법 중 하나는 텍스트 파일을 읽고 쓰는 것입니다. 우리는 텍스트 파일을 읽는 프로그램을 이미 봤습니다; 이 장에서 쓰는 프로그램을 보게 될 것입니다.

대안은 프로그램의 상태를 데이터베이스에 저장하는 것입니다. 이 장에서 간단한 데이터베이스와 프로그램 데이터의 저장을 간단하게 만드는 pickle 모듈을 소개할 것입니다.

읽기와 쓰기

텍스트 파일은 하드 드라이브, 플래시 메모리, 시디롬(CD-ROM) 과 같은 영구적인 매체에 저장된 일련의 문자들입니다. [wordlist] 절에서 파일을 어떻게 열고 읽는지 보았습니다.

파일을 쓰기 위해서는, 두 번째 매개변수로 'w' 모드(mode)를 사용해서 열어야만 합니다:

>>> fout = open('output.txt', 'w')
>>> print fout
<open file 'output.txt', mode 'w' at 0xb7eb2410>

파일이 이미 존재할 때, 쓰기 모드로 열면 예전의 데이터를 모두 지우고 새로 시작하게 되니 조심하세요! 파일이 없으면 새 파일이 생깁니다.

write 메쏘드는 데이터를 파일에 넣습니다.

>>> line1 = "This here's the wattle,\n"
>>> fout.write(line1)

늘 그렇듯이, 파일 객체는 어디쯤 있는지를 기억하기 때문에, write 를 한 번 더 호출하면, 새 데이터를 끝에 붙입니다.

>>> line2 = "the emblem of our land.\n"
>>> fout.write(line2)

쓰기가 끝나면, 파일을 닫아야만 합니다.

>>> fout.close()

형식 연산자

write의 인자는 문자열이어야 하기 때문에, 다른 값들을 파일에 넣고 싶다면, 문자열로 바꿔야만 합니다. 그렇게 하는 가장 쉬운 방법은 str을 쓰는 겁니다:

>>> x = 52
>>> f.write(str(x))

다른 방법은 형식 연산자(format operator), %,를 사용하는 것입니다. 정수에 적용될 때 %는 나머지 연산자입니다. 그러나 첫 번째 피연산자가 문자열일 때, %는 형식 연산자입니다.

첫 번째 피연산자는 형식 문자열(format string)인데, 두 번째 피연산자가 형식화되는 방법을 지정하는 하나나 그 이상의 형식 시퀀스(format sequence)를 포함합니다. 결과는 문자열입니다.

예를 들어, 형식 시퀀스 '%d'는 두 번째 피연산자가 정수(d 는 “십진수(decimal)”를 나타냅니다)로 형식화되어야 함을 뜻합니다:

>>> camels = 42
>>> '%d' % camels
'42'

결과는 문자열 '42'인데, 정수 값 42와는 다릅니다.

형식 시퀀스는 문자열의 어느 곳에건 나타날 수 있기 때문에, 구문 중에 값을 내장시킬 수 있습니다:

>>> camels = 42
>>> 'I have spotted %d camels.' % camels
'I have spotted 42 camels.'

만약 문자열에 여러 개의 형식 시퀀스가 있으면, 두 번째 피연산자는 튜플이 되어야 합니다. 각 형식 시퀀스는 차례대로 튜플의 요소들과 대응됩니다.

다음 예는 '%d' 를 정수를 형식화하는데, '%g' 를 실수(이유는 묻지 마세요)를 형식화하는데, '%s'를 문자열을 형식화하는데 사용합니다:

>>> 'In %d years I have spotted %g %s.' % (3, 0.1, 'camels')
'In 3 years I have spotted 0.1 camels.'

튜플에 있는 요소의 개수는 문자열에 있는 형식 시퀀스의 개수와 일치해야 합니다. 또, 요소들의 형도 형식 시퀀스와 맞아야 합니다.

>>> '%d %d %d' % (1, 2)
TypeError: not enough arguments for format string
>>> '%d' % 'dollars'
TypeError: illegal argument type for built-in operation

첫 번째 예에서는 요소가 모자라고, 두 번째 예에서는 요소의 형이 틀립니다.

형식 연산자는 강력합니다만, 사용하기에 어려울 수 있습니다. http://docs.python.org/2/library/stdtypes.html#string-formatting에서 더 많은 정보를 얻을 수 있습니다.

파일명과 경로

[paths]

파일들은 디렉터리(directory)(“폴더(folder)”라고도 부릅니다)들 안에 정돈됩니다. 실행중인 모든 프로그램은 “현재 디렉터리(current directory)”를 갖는데, 대부분의 연산들에서 내정(default) 디렉터리가 됩니다. 예를 들어, 읽기 위해 파일을 열 때, 파이썬은 현재 디렉터리에서 파일을 찾습니다.

os 모듈은 파일과 디렉터리를 다루는데 필요한 함수들을 제공합니다 (“os”는 “운영 체제(operating system)”을 나타냅니다). os.getcwd 는 현재 디렉터리의 이름을 돌려줍니다:

>>> import os
>>> cwd = os.getcwd()
>>> print cwd
/home/dinsdale

cwd 는 “현재 작업 디렉터리(current working directory)”를 나타냅니다. 이 예에서 결과는 /home/dinsdale인데, dinsdale이라는 사용자의 홈 디렉터리입니다.

cwd와 같이 파일을 지정하는 문자열을 경로(path)라고 부릅니다. 상대 경로(relative path)는 현재 디렉터리에서 시작합니다; 절대 경로(absolute path)는 파일 시스템(file system)의 최 상단 디렉터리에서 시작합니다.

지금까지 우리가 본 경로들은 단순한 파일명이라서, 현재 디렉터리에 상대적입니다. 파일의 절대 경로를 찾으려면, os.path.abspath를 사용하면 됩니다:

>>> os.path.abspath('memo.txt')
'/home/dinsdale/memo.txt'

os.path.exists는 파일이나 디렉터리가 존재하는지 검사합니다:

>>> os.path.exists('memo.txt')
True

존재할 경우에, os.path.isdir은 디렉터리인지 검사합니다:

>>> os.path.isdir('memo.txt')
False
>>> os.path.isdir('music')
True

비슷하게, os.path.isfile은 파일인지 검사합니다.

os.listdir은 주어진 디렉터리에 있는 파일들 (과 다른 디렉터리들)의 리스트를 돌려줍니다:

>>> os.listdir(cwd)
['music', 'photos', 'memo.txt']

이 함수들을 예시하기 위해, 다음 예는 디렉터리를 “탐색(walk)”하면서 모든 파일들의 이름을 인쇄하고, 모든 디렉터리들에 대해 재귀적으로 자신을 호출합니다.

def walk(dirname):
    for name in os.listdir(dirname):
        path = os.path.join(dirname, name)

        if os.path.isfile(path):
            print path
        else:
            walk(path)

os.path.join은 디렉터리와 파일 이름을 받아서 완전한 경로로 결합합니다.

[연습 14.1.]

os 모듈은 walk 라는 이름의 함수를 제공하는데 이 것과 비슷하지만 좀 더 풍부한 기능을 제공합니다. 설명서를 읽고 주어진 디렉터리와 그 하위디렉터리들에 있는 파일들의 이름을 인쇄하는데 사용하세요.

답: http://thinkpython.com/code/walk.py.

예외 잡기

[catch]

여러분이 파일을 읽고 쓰려고 할 때 많은 것들이 잘못될 수 있습니다. 없는 파일을 열려고 하면, IOError를 만나게 됩니다:

>>> fin = open('bad_file')
IOError: [Errno 2] No such file or directory: 'bad_file'

파일에 접근할 수 있는 권한이 없다면:

>>> fout = open('/etc/passwd', 'w')
IOError: [Errno 13] Permission denied: '/etc/passwd'

그리고 만약 디렉터리를 읽기 위해 열려고 한다면, 이렇게 됩니다

>>> fin = open('/home')
IOError: [Errno 21] Is a directory

이런 오류들을 피하기 위해, os.path.exists과 os.path.isfile같은 함수들을 사용할 수 있습니다만, 모든 가능성을 검사하는 데는 많은 시간과 코드가 들어갑니다 (만약 “Errno 21”이 뭔가를 뜻한다면, 잘못될 수 있는 방법이 적어도 21가지 있다는 뜻입니다.).

전진하면서 시도(try)하고 문제가 발생한다면 처리하는 것이 더 좋은 방법인데, try 문이 하는 것이 바로 이것입니다. 문법은 if 문과 유사합니다:

try:
    fin = open('bad_file')
    for line in fin:
        print line
    fin.close()
except:
    print 'Something went wrong.'

파이썬은 try 구절을 실행하는 것으로 시작합니다. 문제가 없다면, except 구절을 건너뛰고 진행합니다. 만약 예외가 발생하면, try 구절에서 빠져 나와 except 구절을 실행합니다.

try 문으로 예외를 다루는 것을 예외를 잡는다(catch)라고 합니다. 이 예에서, except 구절은 그리 도움이 되지 않는 오류 메시지를 인쇄합니다. 일반적으로, 예외를 잡는 것은 문제를 바로잡거나, 다시 시도하거나, 적어도 프로그램을 우아하게 끝낼 기회를 줍니다.

[연습 14.2.]

인자로 패턴(pattern) 문자열과 치환(replacement) 문자열과 두 개의 파일명을 인자로 받아들이는 함수 sed를 작성하세요; 첫 번째 파일을 읽어서 콘텐트를 두 번째 파일에 (필요하면 만드세요) 써야 합니다. 만약 패턴 문자열이 파일에 등장하면, 치환 문자열로 바꿔야 합니다.

만약 파일을 열거나, 읽거나, 쓰거나, 닫을 때 오류가 발생한다면, 예외를 잡아서 오류 메시지를 인쇄한 후 종료해야 합니다. 답: http://thinkpython.com/code/sed.py.

데이터베이스

데이터베이스(database)는 데이터를 저장하기 위해 조직화된 파일입니다. 대부분의 데이터베이스는 키를 값에 대응시킨다는 의미에서 딕셔너리처럼 조직화됩니다. 가장 큰 차이는 데이터베이스의 경우 디스크(또는 다른 영구적 저장소)에 있어서 프로그램 종료 후에도 지속한다는 점입니다.

anydbm 모듈은 데이터베이스 파일들을 만들고 갱신하는 인터페이스를 제공합니다. 예로, 이미지 파일의 캡션(caption)을 저장하는 데이터베이스를 만들겠습니다.

데이터베이스를 여는 것은 다른 파일들을 여는 것과 유사합니다:

>>> import anydbm
>>> db = anydbm.open('captions.db', 'c')

'c' 모드는 이미 존재하지 않을 경우 데이터베이스를 만들어야 한다는 뜻입니다. 결과는 (대부분의 연산에서) 딕셔너리처럼 사용할 수 있는 데이터베이스 객체입니다. 새 항목을 만들면, anydbm은 데이터베이스 파일을 갱신합니다.

>>> db['cleese.png'] = 'Photo of John Cleese.'

항목들 중 하나에 접근하면, anydbm은 파일을 읽습니다:

>>> print db['cleese.png']
Photo of John Cleese.

이미 존재하는 키에 새로 대입하면, anydbm은 예전 값을 대체합니다:

>>> db['cleese.png'] = 'Photo of John Cleese doing a silly walk.'
>>> print db['cleese.png']
Photo of John Cleese doing a silly walk.

많은 딕셔너리 메쏘드들, keys 나 items와 같은,도 데이터베이스 객체에 적용됩니다. for 문으로 순환하는 것도 마찬가지입니다.

for key in db:
    print key

다른 파일들처럼, 사용을 마치면 데이터베이스를 닫아야 합니다:

>>> db.close()

피클링

anydbm의 제약은 키와 값이 문자열이어야 한다는 것입니다. 만약 다른 형을 사용하려고 하면, 오류를 만나게 됩니다.

pickle 모듈이 도움을 줄 수 있습니다. 이 모듈은 거의 모든 종류의 객체를 데이터베이스에 저장하기 적합한 문자열로 변환하고, 문자열을 객체로 역 변환합니다.

pickle.dumps는 액체를 매개변수로 받아서 문자열 표현을 돌려줍니다(dumps 는 “dump string”의 줄임 말입니다):

>>> import pickle
>>> t = [1, 2, 3]
>>> pickle.dumps(t)
'(lp0\nI1\naI2\naI3\na.'

이 형식은 사람이 읽기에 적합하지 않습니다; pickle이 해석하기 용이하게 하기 위함입니다. pickle.loads(“load string”)는 객체를 재구성합니다:

>>> t1 = [1, 2, 3]
>>> s = pickle.dumps(t1)
>>> t2 = pickle.loads(s)
>>> print t2
[1, 2, 3]

새 객체가 예전 것과 같은 값을 갖고 있기는 하지만, (일반적으로) 같은 객체는 아닙니다:

>>> t1 == t2
True
>>> t1 is t2
False

다른 말로 하면, 피클링(pickling)한 후의 언피클링(unpickling)은 객체를 복사하는 것과 같은 효과입니다.

여러분은 문자열이 아닌 것들을 데이터베이스에 저장하기 위해 pickle을 사용할 수 있습니다. 사실, 이 조합은 아주 널리 사용되기 때문에 shelve라는 이름의 모듈에 캡슐화되었습니다.

[연습 14.3.]

연습 [anagrams]에 대한 제 답을 http://thinkpython.com/code/anagram_sets.py에서 다운로드 한다면, 글자들의 정렬된 문자열을 그 글자들로 철자가 구성된 단어들의 리스트로 대응하는 딕셔너리를 만드는 것을 보게 됩니다. 예를 들어, ’opst’ 는 리스트 로 대응됩니다.

anagram_sets 를 들여오고(import) 두 개의 새 함수를 제공하는 모듈을 작성하세요: store_anagrams 은 애너그램(anagram) 딕셔너리를 “쉘프(shelf)”에 저장해야 합니다; read_anagrams 은 단어를 찾아서 애너그램의 리스트를 돌려줘야 합니다. 답: http://thinkpython.com/code/anagram_db.py

파이프

대부분의 운영 체제는 명령 행 인터페이스(command-line interface)를 제공하는데, 쉘(shell)이라고 알려져 있기도 합니다. 쉘은 보통 파일 시스템을 찾아 다니고 애플리케이션을 시동시키는 명령들을 제공합니다. 예를 들어, 유닉스(Unix)에서 cd 로 디렉터리를 변경하고, ls로 디렉터리의 내용을 표시하고, (예를 들어) firefox 를 입력해서 웹 브라우저를 시작시킵니다.

쉘에서 시작시킬 수 있는 모든 프로그램은 파이프(pipe)를 사용해서 파이썬에서도 시작시킬 수 있습니다. 파이프는 실행중인 프로그램을 나타내는 객체입니다.

예를 들어, 유닉스 명령 ls -l은 보통 현재 디렉터리의 내용을 (긴 형식으로) 표시합니다. 여러분은 os.popen [1] 으로 ls를 시작시킬 수 있습니다:

>>> cmd = 'ls -l'
>>> fp = os.popen(cmd)

인자는 쉘 명령어를 포함하는 문자열입니다. 반환 값은 열린 파일과 똑같이 동작하는 객체입니다. 여러분은 ls 프로세스로부터의 출력을 readline으로 한번에 한 줄씩 읽거나 read로 전부 한 번에 읽을 수 있습니다:

>>> res = fp.read()

다 끝나면, 파일처럼 파이프를 닫아줍니다:

>>> stat = fp.close()
>>> print stat
None

반환 값은 ls 프로세스의 최종 상태입니다; None 은 정상적으로 (오류 없이) 종료했다는 뜻입니다.

예를 들어, 대부분의 유닉스 시스템들은 md5sum 이라는 명령을 제공하는데, 파일의 내용을 읽어서 “체크 섬(checksum)”을 계산합니다. MD5에 대한 내용은 http://en.wikipedia.org/wiki/Md5에서 읽을 수 있습니다. 이 명령은 두 파일의 내용이 같은 지를 검사하는 효율적인 방법을 제공합니다. 다른 내용이 같은 체크 섬을 줄 확률은 아주 작습니다(즉, 우주가 붕괴하기 전에 발생할 것 같지 않습니다).

여러분은 파이프로 파이썬에서 md5sum을 실행하고 이런 결과를 얻을 수 있습니다:

>>> filename = 'book.tex'
>>> cmd = 'md5sum ' + filename
>>> fp = os.popen(cmd)
>>> res = fp.read()
>>> stat = fp.close()
>>> print res
1e0033f0ed0656636de0d75144ba32e0  book.tex
>>> print stat
None

[연습 14.4.] [checksum]

MP3 파일들의 커다란 컬렉션에서, 같은 노래가 다른 디렉터리나 다른 파일명으로 저장되어서 여러 개가 있을 수 있습니다. 이 연습의 목표는 중복을 찾는 것입니다.

[1]디렉터리와 하위 디렉터리들을 재귀적으로 검색해서 주어진 확장 자(.mp3와 같은)를 가진 모든 파일들의 완전한 경로들의 리스트를 돌려주는 프로그램을 작성하세요. 힌트: os.path는 파일과 경로의 이름을 조작하는데 유용한 함수들을 여러 개 제공합니다.
[2]중복을 감지하기 위해서, md5sum을 사용해서 각 파일들의 “체크 섬(checksum)’’을 계산할 수 있습니다. 만약 두 파일이 같은 체크 섬을 갖는다면, 아마도 그들은 내용이 같을 것입니다.
[3]이중으로 검사하기 위해, 유닉스 명령어 diff를 사용할 수 있습니다.

답: http://thinkpython.com/code/find_duplicates.py.

모듈 작성하기

[modules]

파이썬 코드를 포함하는 모든 파일은 모듈로 들여오기 할 수 있습니다. 예를 들어, 다음과 같은 코드가 들어있는 wc.py 라는 파일이 있다고 합시다:

def linecount(filename):
    count = 0
    for line in open(filename):
        count += 1
    return count

print linecount('wc.py')

이 프로그램을 실행하면, 자지 자신을 읽어서 파일에 있는 줄의 개수, 7입니다,를 인쇄합니다. 여러분인 이런 식으로 들여오기 할 수도 있습니다:

>>> import wc
7

이제 여러분은 모듈 객체 wc를 얻게 됩니다:

>>> print wc
<module 'wc' from 'wc.py'>

이 모듈은 linecount 라는 함수를 제공합니다:

>>> wc.linecount('wc.py')
7

이 것이 파이썬에서 모듈을 작성하는 방법입니다.

이 예에서 단 한가지 문제는 모듈을 들여오기 할 때 그 끝에 있는 테스트 코드를 실행한다는 것입니다. 보통 모듈을 들여오기 할 때, 새 함수를 정의하기는 하지만 그 것들을 실행하지는 않습니다.

모듈로 들여오기 될 프로그램들은 종종 다음과 같은 관용 표현을 사용합니다:

if __name__ == '__main__':
    print linecount('wc.py')

__name__ 은 프로그램이 시작할 때 설정되는 내장 변수입니다. 만약 프로그램이 스크립트로 실행되고 있다면, __name____main__ 이라는 값을 갖습니다; 그 경우 테스트 코드가 실행됩니다. 그렇지 않으면, 모듈이 들여오기 되는 중이면, 테스트 코드는 건너뜁니다.

[연습 14.5.]

이 예를 wc.py 라는 파일에 입력하고 스크립트로 실행하세요. 그런 다음 파이썬 인터프리터를 실행하고 import wc 하세요. 모듈이 들여오기 중일 때 __name__ 의 값은 뭔가요?

경고: 이미 들여오기 된 모듈을 들여오기 하면, 파이썬은 아무 일도 하지 않습니다. 파일이 변경되었다 하더라도, 파일을 다시 읽지 않습니다.

모듈을 다시 읽어오기를 원한다면, 내장 함수 reload를 사용할 수 있습니다만, 까다로울 수 있어서, 할 수 있는 가장 안전한 것은 인터프리터를 다시 시작시킨 후에 모듈을 다시 들여오기 하는 것입니다.

디버깅

파일을 읽고 쓸 때, 공백문자들과 관련된 문제에 부딪힐 수 있습니다. 이 오류들은 스페이스, 탭, 개행문자들이 보통 보이지 않기 때문에 디버깅하기 어려울 수 있습니다:

>>> s = '1 2\t 3\n 4'
>>> print s
1 2  3
 4

내장 함수 repr이 도움이 될 수 있습니다. 임의의 객체를 인자로 받아 객체의 문자열 표현을 돌려줍니다. 문자열의 경우, 공백 문자들을 역 슬래시 시퀀스로 표현합니다:

>>> print repr(s)
'1 2\t 3\n 4'

이 것은 디버깅에 도움이 될 수 있습니다.

여러분이 만날 또 다른 문제는 서로 다른 시스템들이 줄의 끝을 표현하는데 다른 문자들을 사용한다는 것입니다. 어떤 시스템은 \n 로 표현되는 개행문자(newline)을 사용합니다. 어떤 시스템은 \r 로 표현되는 리턴(return) 문자를 사용합니다. 또 어떤 시스템은 둘 다 사용합니다. 만약 여러분이 다른 시스템으로 옮긴다면, 이런 비 일관성은 문제를 일으킬 수 있습니다.

대부분의 시스템에, 한 형식을 다른 형식으로 바꿔주는 애플리케이션이 있습니다. 여러분은 http://en.wikipedia.org/wiki/Newline에서 그 것들을 찾을 (또는 이 이슈에 대해 더 읽을) 수 있습니다. 또는, 물론, 여러분은 여러분 스스로 하나 만들 수 있습니다.

용어

지속적 persistent:
무기한 실행하고 적어도 데이터의 일부를 영구적인 기억장소에 보관하는 프로그램을 가리키는 표현.
형식 연산자 format operator:
형식 문자열과 튜플을 받아서 형식 문자열에 의해 지정된 방식으로 형식화된 튜플의 요소들을 포함하는 문자열을 만드는 연산자 %.
형식 문자열 format string:
형식 연산자와 함께 사용되는 문자열로 형식 시퀀스들을 포함한다.
형식 시퀀스 format sequence:
형식 문자열에 있는 문자들의 시퀀스, %d과 같은,로 값이 어떻게 형식화되어야 하는지 지정한다.
텍스트 파일 text file:
하드 드라이브와 같은 영구 기억장치에 저장된 문자들의 시퀀스.
디렉터리 directory:
파일들의 이름 붙은 컬렉션으로 폴더(folder)라고도 부른다.
경로 path:
파일을 지정하는 문자열.
상태 경로 relative path:
현재 디렉터리에서 시작하는 경로.
절대 경로 absolute path:
파일 시스템(file system)의 최 상단 디렉터리에서 시작하는 경로.
잡기 catch:
try 와 except 문을 사용해서, 예외가 프로그램을 종료시키는 것을 막는 것.
데이터베이스 database:
값에 대응되는 키를 갖는 딕셔너리처럼 조직화된 내용을 갖는 파일.

연습

[연습 14.6.] [urllib]

urllib 모듈은 URL을 다루고 웹에서 정보를 다운로드 하는 메쏘드들을 제공합니다. 다음 예는 thinkpython.com 에서 비밀 메시지를 다운로드하고 인쇄합니다:

import urllib

conn = urllib.urlopen('http://thinkpython.com/secret.html')
for line in conn:
    print line.strip()

이 코드를 실행하고 보이는 지시를 따르세요. 답: http://thinkpython.com/code/zip_code.py.