CodeOnWeb
로그인

챕터 11. 파일

파이썬에서 파일을 다루어 봅시다.

Park Jonghyun 2015/09/01, 23:24

내용

파이썬 3에 뛰어들기 (22)

챕터 -1. “파이썬 3로 뛰어들기”에서 달라진 점 챕터 0. 파이썬 설치하기 챕터 1. 첫 파이썬 프로그램 챕터 2. 고유 자료형 챕터 3. 컴프리헨션(Comprehensions) 챕터 4. 문자열(Strings) 챕터 5. 정규표현식(Regular Expressions) 챕터 6. 클로저와 제너레이터(Closures & Generators) 챕터 7. 클래스와 반복자(Classes & Iterators) 챕터 8. 고급 반복자(Advanced Iterators) 챕터 9. 단위 테스트(Unit Testing) 챕터 10. 리팩토링(Refactoring) 챕터 11. 파일 챕터 12. XML 챕터 13. 파이썬 객체 직렬화 챕터 14. HTTP 웹서비스 챕터 15. 사례 연구: chardet을 파이썬 3로 이식하기 챕터 16. 파이썬 라이브러리 패키징하기 부록 A. 2to3를 이용해서 코드를 파이썬 3로 이식하기 부록 B. 특수 함수 이름 부록 C. 이제 어디로 가야 할까요? 부록 D. 문제 해결

"9마일(약 14.5km)을 걷는 것은 장난이 아니다, 특히 비와 함께라면 더욱 더."
— 해리 케멜먼(Harry Kemelman), The Nine Mile Walk

뛰어들기

제 노트북에 윈도우만 설치하고 나서 파일의 개수를 세어보니 38,493개 였습니다. 파이썬 3를 설치한 뒤에는 3,000개의 파일이 더 생기더군요. 파일은 대부분의 운영체제에서 주된 저장 패러다임입니다. 그 개념이 너무 깊게 베어들어 있어서 다른 대안은 상상하기도 어려울 겁니다. 여러분의 컴퓨터는 좀 비유적으로 말하자면 파일의 바다에 빠져있다고 할 수 있을지도 모릅니다.

텍스트 파일을 읽기

파일을 읽기 전에 먼저 그 파일을 열어야 합니다. 파이썬에서 파일을 여는 일은 더 이상 쉬울 수가 없을 정도입니다:

파이썬은 파일 이름을 인자로 받는 open()이라는 내장 함수를 가지고 있습니다. 이 예에서는 파일 이름이 'examples/chinese.txt'이네요. 그런데 이 파일 이름에 다섯 가지 정도 흥미로운 점이 있습니다:

  1. 그냥 파일 이름이 아니고 디렉토리 경로를 포함하는 파일 이름입니다. 파일의 경로와 파일 이름을 각각 다른 인자로 받는 가상의 함수를 생각해볼 수도 있지만, open() 함수는 하나로 합쳐서 받습니다. 파이썬에서 "파일 이름"을 쓸 때는 언제나 디렉토리 경로도 함께 써줄 수 있습니다.
  2. 디렉토리 경로는 슬래쉬(/)를 사용합니다. 하지만 제가 어떤 운영체제를 사용하는지 말하지는 않았습니다. 윈도우즈는 역슬래쉬()를 사용하고 Mac OS X이나 리눅스에서는 슬래쉬를 사용하지요. 그런 것과 상관없이, 파이썬에서는 항상 슬래쉬를 사용하고 윈도우즈에서도 아무 문제 없이 돌아갑니다.
  3. 디렉토리 경로가 슬래쉬나 드라이브 문자(C: 같은)로 시작하지 않는데, 이런 경로를 상대 경로(relative path)라 부릅니다. 무엇에 대해서 상대적인지 궁금할 수도 있습니다. 잠시만 기다리세요.
  4. 문자열입니다. 윈도우즈를 포함한 모든 최신 운영체제는 파일 이름과 디렉토리를 저장하기 위해 유니코드를 사용합니다. 파이썬 3는 ASCII가 아닌 경로 이름도 완벽하게 지원합니다.
  5. 파일이 꼭 여러분 컴퓨터의 하드디스크에 저장되어 있을 필요가 없습니다. 네트워크 드라이브에 있는 경로를 써도 문제 없습니다. "파일"은 완전히 가상 파일시스템에 있는 어떤 조각이 될 수도 있습니다. 여러분의 컴퓨터가 파일이라고 인식하고 파일에 접근하는 방식으로 참조할 수 있기만 하다면 파이썬은 그 "파일"을 열 수 있습니다.

open() 함수를 호출할 때 파일 이름 외에 encoding이라는 다른 인자가 더 주어져 있습니다. 오, 여러분, 정말 친숙하게 들리지 않나요.

문자 인코딩, 그 추한 머리를 들다

바이트는 바이트이고, 문자는 추상화된 것입니다. 문자열은 유니코드 문자의 배열이지요. 반면, 디스크에 있는 파일은 바이트의 배열입니다. 그러면 여러분이 디스크에서 "텍스트 파일"을 하나 읽어 들였다고 하면, 파이썬은 그 바이트 배열을 어떻게 문자의 배열로 바꾸는 것일까요? 파이썬은 읽어들인 바이트를 지정된 문자 인코딩 알고리듬에 따라 해석해서 유니코드 문자의 배열(즉, 문자열)로 반환합니다.

무슨 일이 일어난 것일까요? 문자 인코딩을 따로 지정하지 않았으므로 파이썬은 기본 인코딩을 이용합니다. 기본 인코딩은 무엇인가요? Traceback에 출력된 내용을 주의 깊게 보시면 cp1252.py, 즉 CP-1252라는 것을 알 수 있습니다. (CP-1252는 마이크로소프트 윈도우즈에서 흔히 사용되는 인코딩입니다.) CP-1252 문자셋은 이 파일에 있는 문자(아마 한자겠죠?)를 지원하지 않으므로 보기 싫은 UnicodeDecodeError를 발생시키며 파일 읽는 데 문제가 있다는 것을 알려줍니다.

하지만 잠깐만요, 더 나쁜 일이 있습니다! 기본 인코딩은 플랫폼에 따라 다를 수 있으므로 이 예제 코드가 여러분 컴퓨터에서는 잘 작동하다가(여러분의 기본 인코딩이 UTF-8이라면) 다른 사람에게 배포했을 때 문제를 발생시킬 수도 있습니다(기본 인코딩이 CP-1252로 설정되어 있다면요).

기본 인코딩이 무엇인지 알고 싶다면 locale 모듈을 import한 뒤 locale.getpreferredencoding()을 호출하세요. 제 윈도우 랩탑에서는 'cp1252'가 반환되고, 윗 층에 있는 리눅스에서는 'UTF8'이 출력됩니다. 제 집 안에서조차 일관성을 유지할 수 없군요! 여러분의 결과는 또 다를 수 있습니다. 같은 윈도우즈에서도 운영체제의 버전이나 지역/언어 설정에 따라 다른 값을 출력할 수 있습니다. 이것이 파일을 열 때 마다 인코딩을 지정해주는 것이 중요한 이유입니다.

스트림 객체(Stream Objects)

지금까지 우리가 공부한 것은 파이썬이 open()이라는 내장 함수를 가지고 있다는 것 뿐이군요. open() 함수는 스트림 객체라는 것을 반환합니다. 스트림 객체는 문자열 스트림(stream)에 대한 정보를 얻거나 조작할 수 있는 메소드와 속성을 가지고 있습니다.

  1. name 속성은 여러분이 파일을 열 때 open() 함수에 넘겨준 파일 이름입니다. 절대 경로로 알아서 바뀌거나 하지는 않습니다.
  2. 마찬가지로, encoding 속성은 open() 함수를 호출할 때 넘겨준 인코딩 종류를 담고 있습니다. 따로 인코딩을 넘겨주지 않았다면(나쁜 개발자군요!) 인코딩 속성은 locale.getpreferredencoding()의 값을 반환합니다.
  3. mode 속성은 파일이 어떤 모드로 열려 있는지 알려줍니다. 이 파일을 열 때 따로 모드를 지정하지 않았으므로, 파이썬이 기본값인 'r'로 설정합니다. 'r'은 읽기 전용 텍스트 모드를 의미합니다. 이 챕터 뒷 부분에서 살펴보겠지만, 파일의 모드에 따라 서로 다른 목적의 일을 할 수 있습니다. 파일에 원하는 내용을 쓸 수 있는 모드, 파일 끝에 추가하는 모드, 파일을 이진(binary) 형식으로 여는 모드(문자열 대신 파이트를 다루고 싶은 경우) 등이 있습니다.

open() 함수에 대한 문서에서 가능한 모든 모드를 볼 수 있습니다.

텍스트 파일에서 데이터 읽기

내용을 읽기 위해 파일을 열었다면 이후 어떤 시점에 실제로 읽어 들이는 코드가 있어야 하겠지요.

  1. 파일을 연 후(물론 올바른 인코딩으로요) 그 내용을 읽으려면 스트림 객체의 read() 메소드를 호출하기만 하면 됩니다. 문자열이 반환되는군요.
  2. 놀랍게도 파일을 다시 한 번 읽었는데 예외가 발생하지 않습니다. 파이썬은 파일의 끝을 지나서 읽어들이는 것을 오류라고 생각하지 않습니다. 그저 빈 문자열을 반환할 뿐입니다.

다시 한 번 읽어보면 어떨까요?

  1. 여전히 파일의 끝에 있으므로 스트림 객체의 read() 메소드를 다시 호출하더라도 빈 문자열이 반환됩니다.
  2. seek() 메소드를 이용해서 파일의 특정 위치(여기서는 0바이트, 즉 처음)로 옮겨갑니다.
  3. read() 메소드에 인자 하나를 넘겨서 호출할 수도 있습니다. 읽어들이고 싶은 문자의 개수이죠.
  4. 원하신다면 한 번에 문자 하나만 읽어들일 수도 있습니다.
  5. 16 + 1 + 1 = … 20?

다시 한 번 해볼까요.

  1. 17번 째 바이트로 파일 객체의 위치를 옮깁니다.
  2. 문자 하나를 읽습니다.
  3. 몇 번째 바이트에 위치하는지 물었더니 20번 째 바이트에 있다고 하네요.

아직 보고 계신가요? seek()와 tell() 메소드는 파일 객체의 위치를 항상 바이트로 계산을 하지만, read() 메소드는 텍스트로 파일을 열었기에 문자의 수를 기준으로 계산합니다. 한자를 UTF-8로 인코딩 할 때 문자 하나가 여러 바이트를 사용할 수 있습니다. 파일에 있는 각 알파벳 문자는 딱 한 바이트씩의 공간만 차지하므로, 영어만 주로 사용한 사람이라면 seek()와 read() 메소드가 같은 방식으로 동작한다고 착각할 수 있습니다. 하지만 이는 일부 문자에만 해당되는 이야기입니다.

하지만 잠깐 기다리세요. 상황이 더 나빠지고 있습니다.

  1. 18번 째 바이트 위치로 이동해서 문자 하나를 읽습니다.
  2. 왜 실패할까요? 18번 째 바이트에 온전한 문자가 없기 때문입니다. 가장 가까이에 있는 문자는 17 바이트에서 시작해 세 바이트를 차지하고 있습니다. 여러 바이트를 차지하는 문자를 중간부터 읽게 되면 UnicodeDecodeError가 발생하면서 오류가 납니다.

파일 닫기

파일을 여는 것은 시스템 자원을 소모하는 일이고, 열린 파일의 모드에 따라 다른 프로그램에서 그 파일에 접근하지 못할 수도 있습니다. 그래서 파일을 열고 원하는 일을 처리한 후 바로 파일을 닫아주는 것은 중요한 일입니다.

음 뭐 별 거 아니네요. 파일을 닫아도 a_file이라는 스트림 객체는 여전히 존재합니다. close() 메소드를 호출하더라도 객체 자체를 제거하지는 않습니다. 하지만 별 쓸모는 없죠.

  1. 닫힌 파일에서 내용을 읽어들일 수 없습니다. IOError 예외가 발생하네요.
  2. 닫힌 파일에서는 seek() 메소드도 작동하지 않습니다.
  3. 닫힌 파일에는 현재 위치가 없으므로 tell() 메소드도 작동하지 않습니다.
  4. 놀랍게도 이미 닫힌 스트림 객체에 close() 메소드를 한 번 더 호출해도 예외가 발생하지 않습니다. 아무 일도 하지 않을 뿐입니다.
  5. 닫힌 스트림 객체는 크게 유용한 속성을 가지고 있지 않습니다. 하지만 closed 속성은 파일이 닫혔는지 아닌지 확인할 때 사용할 수 있습니다.

파일을 자동으로 닫기

스트림 객체가 close() 메소드를 가지고 있긴 하지만, 만약 여러분의 코드에 버그가 있어 close()를 호출하기 전에 프로그램이 갑자기 끝난다면 어떻게 될까요? 이론적으로 그 파일은 필요한 시간보다 훨씬 긴 시간 동안 열려있게 될 겁니다. 여러분의 컴퓨터에서 디버깅을 할 때는 별 문제가 아니지만 실제 프로덕션 서버에서라면 문제가 될 수 있습니다.

파이썬 2는 try..finally 블록으로 이 문제를 해결합니다. 파이썬 3에서도 여전히 사용할 수 방법이라, 좀 오래되었거나 파이썬 3로 포팅된 코드에서 종종 찾아볼 수 있을겁니다. 하지만 파이썬 2.6 버전에서 with 문을 이용한 좀 더 깔끔한 해결책이 등장했는데, 파이썬 3에서는 이 방법이 선호됩니다.

이 코드에서 open()이 호출되었는데 a_file.close()는 어디에도 보이지 않네요. with 문은 if 문이나 for 루프처럼 코드 블록을 시작합니다. 코드 블록 안에서 변수 a_file은 open() 메소드를 호출해서 반환된 스트림 객체를 가리킵니다. 일반적인 스트림 객체의 메소드(seek(), read() 등)를 사용한 것을 볼 수 있습니다. 마침내 with 블록이 끝나면 파이썬은 자동으로 a_file.close()를 호출합니다.

이 방식의 좋은 점은 프로그램이 비정상적으로 종료될 때에도 파일이 닫힌다는 것입니다. 여러분의 코드가 예상치 못한 예외를 발생시키고 프로그램 전체가 끼익 소리를 내며 멈추어도 파일은 닫힙니다. 확실하게요.

좀 더 기술적으로 말하자면 with 문은 런타임 콘텍스트(runtime context)라는 것을 생성합니다. 그러면 스트림 객체는 콘텍스트 관리자(context manager)처럼 작동합니다. 파이썬은 스트림 객체인 a_file을 생성한 뒤 런타임 콘텍스트로 진입한다는 것을 알려줍니다. with 코드 블록이 끝나면 파이썬은 스트림 객체에게 런타임 콘텍스트를 이제 빠져나간다고 알려주고, 그러면 스트림 객체는 자신의 close() 메소드를 호출하게 됩니다. 더 자세한 것은 부록 B의 "with 블록에서 사용할 수 있는 클래스" 섹션을 참고해주세요.

with 문은 꼭 파일을 다룰 때만 쓸 수 있는 것은 아닙니다. 런타임 콘텍스트를 생성하고 객체에게 런타임 콘텍스트에 진입하거나 다시 빠져 나온다는 사실을 알려줄 필요가 있는 상황에 쓰일 수 있는 범용적인 프레임워크입니다. 그 객체가 스트림 객체면 파일과 관련된 유용한 일(파일을 자동으로 닫는다던지)을 하는 것 뿐이지요. 그런 일 자체는 스트림 객체에 정의되어 있으며 with 문과는 관계가 없습니다. 파일이 아닌 다른 작업에 콘텍스트 관리자를 사용할 수 있는 방법은 많습니다. 심지에 여러분이 직접 만들 수도 있습니다. 이에 대해서는 이 챕터 후반부에 더 이야기 하겠습니다.

한 번에 한 줄씩 데이터 읽어들이기

텍스트 파일의 "한 줄"은 여러분이 생각하는 그대로의 의미입니다. 몇 개의 단어를 입력하고 ENTER 키를 누르면 새로운 줄이 생기지요. 텍스트 한 줄은 여러 개의 문자가 나열된 것인데... 그런데 한 줄이 끝난다고 알려주는 개행(new line) 문자는 뭔가요? 음 약간 복잡한데요, 텍스트 파일에서 한 줄의 끝을 지시하는 문자는 운영체제마다 다르기 때문입니다. 어떤 운영체제에서는 캐리지 리턴(carriage return, CR)을 쓰고, 다른 운영체제에서는 라인 피드(line feed, LF)를 씁니다. CR과 LF를 연달아 나오는 것을 한 줄의 끝으로 인식하는 경우도 있습니다.

하지만 파이썬이 다양한 방식의 개행 문자를 알아서 처리하므로 너무 걱정하지 말고 안도의 한숨을 내쉬어도 됩니다. "이 텍스트 파일에서 한 번에 한 줄씩 읽고 싶어"라고 파이썬에게 지시하면, 그 텍스트 파일이 어떤 개행 문자를 사용하는지 파악하고 알아서 처리합니다.

개행 문자의 종류를 직접 설정하고 싶다면 open() 함수의 newline 인자에 넘겨면 됩니다. 자세한 것은 open() 함수에 대한 공식 문서를 보십시오.

그럼 사용을 한 번 해볼까요. 파일의 내용을 한 번에 한 줄씩 읽는 것 말이에요. 아주 간단해요. 아름답습니다.

oneline.py 다운로드 하기

  1. with 패턴을 이용해서 파일을 안전하게 열고 있습니다. 필요할 때 파이썬이 알아서 파일을 닫아줍니다.
  2. 한 번에 한 줄씩 읽기 위해 for 순환문을 사용했습니다. 그렇습니다. 명시적으로 read()와 같은 메소드를 사용할 수도 있지만, 스트림 객체가 반복자(iterator)라는 점을 이용해 이런 식으로 사용할 수도 있습니다.
  3. 문자열 메소드인 format()을 이용해 줄 번호와 내용을 출력합니다. 서식 지정자인 {:>4}는 "인자를 네 칸의 공간에 우측으로 정렬해서 출력하라"는 의미입니다. a_line 변수는 캐리지 리턴 등을 포함한 한 줄의 모든 문자를 포함하고 있습니다. rstrip() 문자열 메소드는 줄 마지막에 붙어 있는 이런 공백 문자(캐리지 리턴 포함)를 제거해줍니다.

혹시 아래와 같은 오류가 발생하나요?

그렇다면 여러분은 아마 파이썬 3.0을 사용하고 계실겁니다. 파이썬 3.1로 업그레이드 하실 것을 강력히 권하고 싶습니다.

파이썬 3.0도 문자열 서식을 지원하지만 서식 지정자에 위치 인덱스를 명시적으로 적어줘야만 합니다. 파이썬 3.1에서는 이 인덱스를 생략할 수 있지요. 파이썬 3.0을 계속 쓰시겠다면 코드를 다음과 같이 바꿔주세요.

텍스트 파일에 쓰기

파일에서 내용을 읽어왔던 것과 비슷한 빙식으로 파일에 내용을 써줄 수도 있습니다. 먼저 파일을 열어 스트림 객체를 얻은 후, 그 객체의 메소드를 이용해 파일에 원하는 내용을 쓰고, 모든 일이 끝나면 파일을 닫습니다.

쓰기 위해 파일을 열려면 open() 함수를 사용할 때 쓰기 모드를 지정해주면 됩니다. 쓰기 모드는 두 가지가 있습니다.

  • "쓰기(write)" 모드는 파일을 덮어씁니다. mode='w'를 open() 함수의 인자로 넘기면 도닙디ㅏ.
  • "추가(append)" 모드는 (이미 존재하는) 파일의 끝에 내용을 추가합니다. mode='a'를 open() 함수의 인자로 넘겨줍니다.

열려는 파일이 존재하지 않을 경우 두 모드 모두 파일을 새로 만듭니다. 그러니 "만약 파일이 없으면 빈 파일 하나를 새로 만들고 처음으로 내용을 채워넣기 위해 열어라"는 식으로 일일이 소란을 떨 필요가 없습니다. 그냥 파일 하나를 열고 바로 쓰기 시작하면 됩니다.

파일에 쓰는 일이 끝나자마자 파일을 닫아야 합니다. 파일과 파일을 여는 데 사용한 자원을 반환하고 데이터가 실제로 디스크에 써진 것을 확실히 하기 위해서입니다. 파일에서 데이터를 읽어올 때와 마찬가지로 스트림 객체의 close() 메소드를 이용하거나 with 문을 이용해 파이썬이 알아서 파일을 닫도록 할 수 있습니다. 제가 어떤 방식을 추천할지는 모두 알고 계시죠?

  1. test.log라는 이름의 파일을 새로 생성해(혹은 이미 있는 파일을 덮어쓰고) 열면서 힘찬 코딩을 시작합시다. mode='w' 인자는 파일을 쓰기 모드로 열라는 의미입니다. 같은 이름의 파일이 이미 존재하고 있다면 그 파일의 데이터는 모두 없어지므로, 파일을 쓸 때는 주의가 필요합니다.
  2. 새로 연 파일에 어떤 내용을 쓰려면 스트림 객체의 메소드 중 하나인 write()를 이용할 수 있습니다. with 블록이 끌나면 파이썬이 파일을 알아서 닫습니다.
  3. 재밌군요. 다시 한 번 더 해봅시다. 하지만 이번에는 mode='a' 인자를 넘겨서 이미 존재하는 test.log 파일에 내용을 추가할 것입니다. 파일의 끝에 새로운 내용을 추가하는 것이므로, 기존 내용이 없어지지는 않습니다.
  4. 이제 test.log 파일에는 원래 있던 내용에 더해 새로 추가한 내용도 같이 저장되어 있을 것입니다. 캐리지 리턴이나 라인 피드 문자는 파일에 써주지 않았다는 점을 기억하세요. 파일에 써줄 때 명시적으로 써주지 않으면 자동으로 저장되거나 하지는 않습니다. 캐리지 리턴은 문자 '\r'로, 라인 피드는 문자 '\n'으로 표현할 수 있습니다. 둘 중 아무 것도 써주지 않았기에 여러분이 기록한 내용은 모두 한 줄로 처리됩니다.

다시 문자 인코딩으로

내용을 쓰기 위해 파일을 열려고 open() 함수를 호출할 때 인코딩 인자를 넘겨준 것을 발견하셨나요? 중요합니다. 절대 생략하지 마세요. 이 챕터 초반에도 언급했지만 파일에는 문자열이 아니라 바이트가 기록됩니다. 텍스트 파일에서 "문자열"을 읽을 수 있는 것처럼 보이는 이유는 인코딩을 지정해서 바이트를 문자열로 해독하기 때문입니다. 텍스트를 파일에 기록할 때도 마찬가지입니다. 파일에는 문자열을 기록할 수 없습니다. 문자열은 추상화된 것입니다. 파일에 무언가를 쓰려면 여러분의 문자열을 어떻게 바이트로 바꿀 것인지 파이썬에게 알려줘야 합니다. 가장 확실한 방법은 파일을 쓰기 모드로 열 때에도 인코딩 인자를 명시해주는 것입니다.

이진 파일(Binary Files)

모든 파일이 텍스트 파일은 아닙니다. 어떤 파일은 제 강아지의 그림일 수도 있겠죠.

  1. 파일은 이진 모드(binary mode)로 여는 것은 쉽습니다. 텍스트 모드로 열 때와 다른 유일한 점은 모드를 지정하는 인자에 'b' 문자를 포함한다는 것 뿐입니다.
  2. 이진 모드로 파일을 열어서 얻게 되는 스트림 객체는 텍스트 스트림 객체와 거의 유사한 속성을 가지고 있습니다. mode 속성은 open() 함수에 넘겨준 모드 인자에 대한 정보입니다.
  3. 이진 스트림 객체는 name 속성도 가지고 있습니다. 텍스트 스트림 객체와 같지요.
  4. 그렇다고 모든 것이 같지는 않습니다. 이진 스트림 객체는 인코딩 인자가 없습니다. 당연하겠지요. 텍스트 모드와는 달리 문자열이 아닌 바이트 자체를 읽거나 쓰는 것이니 따로 뭘 변환할 필요가 없습니다. 이진 파일은 여러분이 써준 그대로 읽어옵니다. 변환은 없습니다.

제가 바이트를 읽는다고 말했나요? 네 물론 그렇습니다.

  1. 텍스트 파일과 유사하게 이진 파일도 한 번에 한 비트씩 읽을 수 있습니다. 하지만 중요한 차이가 하나 있죠.
  2. ... 여러분이 문자열이 아닌 바이트를 읽고 있다는 점이요. 이진 모드로 파일을 열었으므로 read() 메소드는 읽어들일 문자의 수가 아니라 읽어들일 바이트의 수를 인자로 받습니다.
  3. 이 말은 read() 메소드에 넘겨주는 숫자와 tell() 메소드를 통해 얻는 스트림 객체의 위치에 차이가 없다는 것입니다. read() 메소드는 바이트를 읽고 seek()과 tell() 메소드는 읽은 바이트 수를 추적하고 있지요. 이진 파일에서는 세 함수 모두 같은 형식의 값을 다룹니다.

파일 이외의 스트림 객체

여러분이 라이브러리를 작성하고 있는데, 그 라이브러리의 함수 중 하나가 파일에서 어떤 데이터를 읽어들이는 기능을 한다고 해봅시다. 그 함수는 문자열로 된 파일 이름을 인자로 받고, 읽기 모드로 파일을 연 후, 원하는 내용을 읽고, 파일을 닫으면서 끝날겁니다. 하지만 그렇게 하면 안 됩니다. 여러분의 API는 파일의 이름 대신 임의의 스트림 객체를 인자로 받아야 합니다.

가장 간단한 형태의 스트림 객체는 문자열을 반환하는 read() 메소드를 가지고 있는 것입니다. 읽어 들일 문자/바이트 수를 지정하는 인자인 size를 넘겨주지 않으면 read() 메소드는 스트림 메소드가 담고 있는 모든 내용을 하나의 값으로 반환합니다. size 인자에 값을 넘겨주면 그 크기 만큼의 데이터만 읽어서 반환하지요. 그 후 다시 read()를 호출하면 마지막으로 읽은 위치부터 데이터를 반환합니다.

파일을 열면서 얻었던 스트림 객체와 정확히 같습니다. 하지만 파일 이름을 인자로 받아 스트림 객체를 직접 생성하는 것과 차이가 하나 있는데, 스트림 객체를 인자로 받으면 실제 파일에 구속되지 않을 수 있습니다. 파일 뿐만 아니라 읽어들일 수 있는 입력 소스라면 어떤 것이든 다룰 수 있게 됩니다. 웹 페이지, 메모리에 저장된 문자열, 심지어 다른 프로그램의 출력도요. 여러분의 함수가 스트림 객체를 받으면, 그 객체의 read() 메소드를 호출하는 것만으로 내용을 읽을 수 있습니다. 그래서 입력 소스의 형태에 상관없이 파일을 다루는 것과 같은 코드를 작성하면 됩니다.

  1. io 모듈은 StringIO라는 클래스를 정의하고 있는데, 메모리에 있는 문자열을 파일처럼 다룰 수 있게 해주는 역할을 합니다.
  2. 문자열을 이용해 스트림 객체를 만드려면 io.StringIO() 클래스를 개체화 하면서 파일처럼 사용하고 싶은 문자열을 인자로 넘기면 됩니다. 이제 스트림 객체가 만들어졌으므로 스트림 객체에 하던 온갖 일들을 똑같이 할 수 있습니다.
  3. read() 메소드를 호출해서 전체 "파일"을 "읽어"들입니다. 이 경우 StringIO 객체가 반환하는 것은 a_string의 내용 전체이죠.
  4. 실제 파일처럼 read() 메소드를 다시 호출하면 빈 문자열이 돌아옵니다.
  5. StringIO 객체의 seek() 메소드를 이용해서 문자열의 첫 부분으로 위치를 이동할 수 있습니다. 파일 예제에서 하던 것과 똑같죠?
  6. read() 메소드에 size 인자를 넘겨서 문자열의 일부만 읽어올 수도 있습니다.

io.StringIO는 문자열을 텍스트 파일처럼 취급할 수 있게 해줍니다. io.BytesIO 클래스도 있는데, 이름에서 짐작할 수 있듯이 바이트 배열을 이진 파일로 다룰 수 있게 해주는 클래스입니다.

압축 파일 다루기

파이썬 표준 라이브러리는 압축 파일을 읽고 쓰는 모듈도 제공합니다. 세상에는 다양한 압축 방법이 있는데, 그 중 윈도우즈가 아닌 플랫폼에서 가장 많이 쓰이는 압축 알고리즘으로 gzipbzip2가 있습니다. (PKZIP이나 GNU Tar로 압축된 파일도 보신 적이 있을 것 같습니다. 파이썬은 이와 관련된 모듈도 가지고 있습니다.)

gzip 모듈을 이용해 gzip으로 압축된 파일을 읽고 쓰기 위한 스트림 객체를 생성할 수 있습니다. 그리고 그 스트림 객체는 read() 메소드와(읽기 모드로 열었을 겅우)와 write() 메소드(쓰기 모드로 열었을 경우)를 제공합니다. 즉, 일반 파일을 읽거나 쓸 때 사용했던 방법을 gzip 압축 파일에도 그대로 적용할 수 있다는 의미입니다. 압축이 해제된 데이터를 저장하기 위한 임시 파일을 따로 만들 필요가 없습니다.

with 문도 제공하므로 일이 끝나면 파이썬이 gzip으로 압축된 파일을 자동으로 닫게 할 수도 있습니다. 아주 좋습니다!

  1. gzip으로 압축된 파일은 항상 이진 모드로 열어야 합니다. (mode 인자에 'b' 문자를 확인하세요.) 이제 커맨드 라인을 좀 써야겠네요.
  2. 저는 이 예제를 리눅스에서 실행했습니다. 커맨드 라인에 익숙하지 않은 분을 위해 말씀드리면, 이 줄은 gzip으로 우리가 압축한 파일에 대한 자세한 정보를 보여달라는 명령입니다. 이 정보를 통해 파일이 잘 생성되었고(out.log.gz) 그 크기는 79바이트라는 사실을 알 수 있습니다. 그런데 여러분이 입력한 문자열보다 크기가 더 크군요! gzip 파일은 파일을 앞 부분에 압축과 관련된 메타정보를 담고 있는 헤더를 생성하므로, 아주 작은 크기의 파일에 적용하면 비효율적입니다.
  3. 리눅스의 gunzip 명령은 gzip으로 압축된 파일을 푼 뒤, 그 내용을 원래 압축 파일에서 .gz 확장자를 제거한 이름의 파일로 저장합니다.
  4. 리눅스의 cat 명령은 파일을 내용을 보여줍니다. 이 파일은 여러분이 out.log.gz 압축파일을 만들 때 입력했던 원래 문자열을 잘 담고 있습니다.

혹시 다음과 같은 오류를 만났나요?

그렇다면 여러분은 아마 파이썬 3.0을 사용하고 계실겁니다. 파이썬 3.1로 업그레이드 하실 것을 강력히 권하고 싶습니다.

파이썬 3.0은 gzip 모듈을 가지고 있지만 gzip 파일 객체를 콘텍스트 매니저로 취급하는 것은 허용하지 않습니다. 파이썬 3.1에서는 gzip 파일 객체를 with 문에 사용할 수 있습니다.

표준 입력, 출력, 오류

커맨드 라인을 잘 다루는 분이라면 표준 입력(standard input), 표준 출력(standard output), 표준 오류(standard error)라는 개념에 이미 익숙하실 겁니다. 그렇지 않으신 분은 이 섹션의 내용을 잘 읽어주시면 됩니다.

표준 출력과 표준 오류는(흔히 stdout, stderr로 표시합니다) Mac OS X과 리눅스를 포함한 모든 유닉스 유사(Unix-like) 시스템에서 제공하는 파이프(pipes)입니다. print() 함수를 호출하면 여러분이 출력하고자 하는 내용은 stdout 파이프로 보내집니다. 여러분의 프로그램에 문제가 생겨 traceback을 출력하면 stderr 파이프로 가게 됩니다. 기본적으로 이 두 가지 파이프는 여러분이 현재 사용하고 있는 터미널과 연결되어 있습니다. 그래서 여러분의 프로그램이 무언가 출력하거나 traceback을 내뿜으면 그 내용을 터미널 윈도우에서 볼 수 있는 것입니다. 그래픽 파이썬 쉘에서는 stdout과 stderr 파이프가 "대화형 윈도우"에 연결되어 있습니다. CodeOnWeb에서는 코드 실행 버튼 위쪽에 별도의 창을 만들어 보여주고 있습니다.

  1. print() 함수가 순환문 안에 있네요. 별 건 없죠.
  2. stdout은 sys 모듈에 정의되어 있는 스트림 객체입니다. stdout의 write() 함수를 호출하면 인자로 넘겨주는 문자열을 출력하고 그 길이를 반환합니다. 사실 print() 함수가 하는 일이 이것이죠. 여러분이 넘겨주는 문자열의 끝에 캐리지 리턴을 추가하고 sys.stdout.write를 호출하는 것입니다.
  3. 가장 간단한 경우 sys.stdout과 sys.stderr은 같은 장소로 출력합니다. 파이썬 IDE나 터미널과 등으로요. 표준 출력과 마찬가지로 표준 오류는 문자열을 끝에 캐리지 리턴을 추가하지 않습니다. 추가하기를 원한다면 직접 추가해야 합니다.

sys.stdout과 sys.stderr은 스트림 객체이지만 읽기는 불가능하고 쓰기만 가능합니다. read() 메소드를 호출하면 IOError를 발생시킵니다.

표준 출력을 리다이렉트(redirect) 하기

sys.stdout과 sys.stderr은 쓰기만 지원하는 스트림 객체입니다. 하지만 상수는 아니고 변수이지요. 즉, 그 객체에 새로운 값(다른 스트림 객체)을 할당해서 출력을 리다이렉트(새 객체로 값을 돌려 출력하는 것)할 수 있다는 말입니다.

stdout.py을 다운로드 하기

커맨드 라인에서 아래처럼 확인해보세요.

혹시 다음과 같은 오류가 나오나요?

그렇다면 여러분은 아마 파이썬 3.0을 사용하고 계실겁니다. 파이썬 3.1로 업그레이드 하실 것을 강력히 권하고 싶습니다.

파이썬 3.0은 with 문을 지원하지만 단 하나의 콘텍스트 관리자만 사용할 수 있습니다. 파이썬 3.1에서는 하나의 with 문에 여러 개의 콘텍스트 관리자를 연결해서 사용할 수 있습니다.

마지막 부분을 먼저 볼까요.

좀 복잡한 with 문이군요. 좀 더 알아보기 쉽게 다시 써볼까요.

다시 써보니 실은 두 개의 with 문이 중첩 사용된 것을 알 수 있네요. 바깥쪽의 with 문은 이제 익숙하리라 생각합니다. out.log라는 UTF-8 인코딩 텍스트 파일을 쓰기 모드로 연 뒤 스트림 객체를 a_file이라는 변수에 할당해줍니다. 그런데 이번에는 여기서 끝이 아닙니다.

as는 어디 있을까요? 사실 with 문에 as가 반드시 있어야 되는 건 아닙니다. 함수를 호출했을 때 반환되는 값을 그냥 무시할 수 있는 것처럼, with 문을 쓸 때에도 콘텍스트 변수에 값을 할당하지 않고 무시할 수 있습니다. 이 예의 경우 RedirectStdoutTo 콘텍스트가 만들어 내는 변화에만 관심이 있으므로 as는 생략되었습니다.

그런데 무슨 변화가 생기는걸까요? RedirectStdoutTo 클래스의 내부를 한 번 들여다봅시다. 이 클래스는 여러분이 직접 작성한 콘텍스트 관리자입니다. 어떤 클래스건 __enter__()와 __exit__() 두 가지 메소드를 정의하기만 하면 콘텍스트 관지라로 사용할 수 있습니다.

  1. __init__() 메소드는 개체가 만들어지는 순간 호출됩니다. 콘텍스트의 영향력이 유지되는 동안 표준 출력으로 사용할 스트림 객체 하나를 인자로 받고 있습니다. 받은 스트림 객체를 개체 변수 self.out_new에 할당해서 다른 메소드가 사용할 수 있게 저장해둡니다.
  2. __enter__() 메소드는 특별한 종류의 클래스 메소드입니다. 파이썬은 콘텍스트에 진입할 때(즉, with 문이 시작될 때) 이 메소드를 호출하지요. 이 메소드는 sys.stdout의 현재 값을 나중을 위해self.out_old 변수에 저장한 뒤, self.out_new를 sys.stdout에 할당해서 표준 입력을 리다이렉트합니다.
  3. __exit__() 메소드도 특별한 종류의 클래스 메소드입니다. 파이썬은 콘텍스트에서 빠져나갈 때(즉, with 문이 끝날 때) 이 메소드를 호출하지요. 이 메소드는 저장해두었던 self.out_old를 sys.stdout에 다시 할당해서 표준 입력을 원래대로 되돌려줍니다.

최종적으로:

  1. 이 print() 문은 "대화형 윈도우"나 터미널(커맨드 라인에서 스크립트를 돌린다면)로 문자열을 출력합니다.
  2. 이 with 문은 쉼표로 구분된 콘텍스트 여러 개를 불러옵니다. 첫 번째 마주치는 콘텍스트는 "바깥" 블록으로, 마지막 콘텍스트는 "안쪽" 블록이 됩니다. 여기서 첫 번째 콘텍스트는 파일을 열고, 두 번째 콘텍스트는 sys.stdout을 첫 번째 콘텍스트에서 생성된 스트림 객체로 리다이렉트합니다.
  3. 이 print() 함수는 with 문에 의해 생성된 콘텍스트 내에서 실행되므로, 화면으로 출력하는 대신 out.log 파일로 출력하게 됩니다.
  4. with 문은 끝났습니다. 파이썬은 콘텍스트를 빠져나가면서 각 콘텍스트에게 필요한 일을 처리하라고 호출합니다. 콘텍스트 관리자는 LIFO(last-in-first-out) 구조의 스택으로 구성되어 가장 마지막에 들어온 콘텍스트에서 가장 먼저 빠져나갑니다.두 번째 콘텍스스에서 sys.stdout을 원래 값으로 되돌린 후 첫 번째 콘텍스트는 out.log 파일을 닫습니다. 표준 출력이 원래대로 돌아왔으므로 print() 함수를 호출하면 다시 화면으로 출력합니다.

표준 오류를 리다이렉트 하는 것도 동일합니다. sys.stdout 대신 sys.stderr을 이용하기만 하면 됩니다.

더 읽을거리


이 강의는 영어로 된 원문을 기초로 작성되었으며, Creative Commons Attribution Share-Alike 라이센스하에 자유로운 변경, 배포가 가능합니다

10452 읽음
이전 챕터 10. 리팩토링(Refactoring)
다음 챕터 12. XML

저자

토론이 없습니다

Please log in to leave a comment

16.5.11618.20190612.allo
문제 보고 · 사용조건 · 개인정보보호
래블업 주식회사 · 대한민국 서울 테헤란로 145 · 대표: 신정규 · 사업자번호: 864-88-00080 · +82 70-8200-2587

거절 확인

닫기
좋아요 책갈피 토론

아래 주소를 복사하세요