Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 3116 – 새로운 I/O

Author:
Daniel Stutzbach <daniel at stutzbachenterprises.com>, Guido van Rossum <guido at python.org>, Mike Verdone <mike.verdone at gmail.com>
Status:
Final
Type:
Standards Track
Created:
26-Feb-2007
Python-Version:
3.0
Post-History:
26-Feb-2007

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

근거와 목표

Python은 read()와 write() 호출로 사용할 수 있는 다양한 스트림 형태(파일 형태라고도 함)의 객체를 허용합니다. read()와 write()를 제공하는 것은 무엇이든 스트림 유사 객체입니다. 그러나 readline()이나 seek() 같은 더 특수하면서도 매우 유용한 함수는 스트림 유사 객체마다 제공될 수도 있고 제공되지 않을 수도 있습니다. Python에는 버퍼링 기능과 텍스트 처리 기능을 더할 수 있는 기본적인 바이트 기반 I/O 스트림에 대한 명세가 필요합니다.

원시 바이트 기반 I/O 인터페이스를 정의하고 나면, 어떤 바이트 기반 I/O 클래스 위에도 버퍼링 계층과 텍스트 처리 계층을 더할 수 있습니다. 같은 버퍼링 및 텍스트 처리 로직을 파일, 소켓, 바이트 배열, 또는 Python 프로그래머가 개발한 사용자 정의 I/O 클래스에 사용할 수 있습니다. 스트림의 표준 정의를 마련하면 read()와 write() 같은 스트림 기반 연산을 fileno()와 isatty() 같은 구현별 연산과 분리할 수 있습니다. 이는 프로그래머가 스트림을 스트림으로서 사용하는 코드를 작성하도록 장려하며, 모든 스트림이 파일 전용 연산이나 소켓 전용 연산을 지원하도록 요구하지 않게 합니다.

새로운 I/O 명세는 Java I/O 라이브러리와 비슷하되, 전반적으로 덜 혼란스럽도록 의도되었습니다. 새로운 I/O 세계를 굳이 파고들고 싶지 않은 프로그래머는 open() 팩토리 메서드가 기존 방식의 파일 객체와 하위 호환되는 객체를 만들어 줄 것으로 기대할 수 있습니다.

명세

Python I/O 라이브러리는 원시 I/O 계층, 버퍼링된 I/O 계층, 텍스트 I/O 계층의 세 계층으로 구성됩니다. 각 계층은 추상 베이스 클래스로 정의되며, 이 클래스에는 여러 구현이 있을 수 있습니다. 원시 I/O 계층과 버퍼링된 I/O 계층은 바이트 단위를 다루고, 텍스트 I/O 계층은 문자 단위를 다룹니다.

원시 I/O

원시 I/O의 추상 베이스 클래스는 RawIOBase입니다. 적절한 운영 체제 시스템 호출을 감싸는 래퍼인 여러 메서드를 가지고 있습니다. 이 함수들 중 어떤 것이 해당 객체에서 의미가 없다면, 구현은 IOError 예외를 발생시켜야 합니다. 예를 들어, 파일이 읽기 전용으로 열려 있다면 .write() 메서드는 IOError를 발생시킵니다. 또 다른 예로, 객체가 소켓을 나타낸다면 .seek(), .tell(), .truncate()는 IOError를 발생시킵니다. 일반적으로 이 함수들 중 하나를 호출하면 정확히 하나의 운영 체제 시스템 호출에 대응합니다.

.read(n: int) -> bytes
객체에서 최대 n 바이트를 읽어 반환합니다. 운영 체제 시스템 호출이 n 바이트보다 적게 반환하면 n 바이트보다 적은 바이트가 반환될 수 있습니다. 0바이트가 반환되면 파일의 끝을 나타냅니다. 객체가 논블로킹 모드이고 읽을 수 있는 바이트가 없으면, 호출은 None을 반환합니다.

.readinto(b: bytes) -> int

객체에서 최대 len(b) 바이트를 읽어 b에 저장하고, 읽은 바이트 수를 반환합니다. .read와 마찬가지로 len(b) 바이트보다 적게 읽을 수 있으며, 0은 파일의 끝을 나타냅니다. 논블로킹 객체에 읽을 수 있는 바이트가 없으면 None이 반환됩니다. b의 길이는 절대 바뀌지 않습니다.

.write(b: bytes) -> int

기록한 바이트 수를 반환하며, 이 값은 < len(b)일 수 있습니다.

.seek(pos: int, whence: int = 0) -> int

.tell() -> int

.truncate(n: int = None) -> int

.close() -> None

그 밖에 몇 가지 메서드를 추가로 정의합니다.

.readable() -> bool
객체가 읽기용으로 열렸으면 True를, 그렇지 않으면 False를 반환합니다. False이면 .read()를 호출할 때 IOError가 발생합니다.

.writable() -> bool

객체가 쓰기용으로 열렸으면 True를, 그렇지 않으면 False를 반환합니다. False이면 .write()와 .truncate()를 호출할 때 IOError가 발생합니다.

.seekable() -> bool

객체가 (디스크 파일처럼) 임의 접근을 지원하면 True를, (소켓, 파이프, tty처럼) 순차 접근만 지원하면 False를 반환합니다. False이면 .seek(), .tell(), .truncate()를 호출할 때 IOError가 발생합니다.

.__enter__() -> ContextManager

컨텍스트 관리 프로토콜입니다. self를 반환합니다.

.__exit__(...) -> None

컨텍스트 관리 프로토콜입니다. .close()와 같습니다.

RawIOBase 구현이 하부 파일 디스크립터 위에서 동작하는 경우에, 그리고 오직 그 경우에만 .fileno() 멤버 함수를 추가로 제공해야 합니다. 이 함수는 구현에서 직접 정의할 수도 있고, 믹스인 클래스를 사용할 수도 있습니다(이 부분은 결정이 필요합니다).

.fileno() -> int
하부 파일 디스크립터(정수)를 반환합니다.

처음에는 RawIOBase 인터페이스를 구현하는 세 가지 구현, 즉 FileIO, SocketIO(소켓 모듈에 있음), ByteIO가 제공될 예정입니다. 사용자가 제공한 정보만으로는 충분하지 않을 수 있으므로(open("/dev/tty", "rw")나 open("/tmp/named-pipe", "rw")를 생각해 보십시오), 각 구현은 객체가 임의 접근을 지원하는지 스스로 판단해야 합니다. 예를 들어 FileIO는 seek() 시스템 호출을 호출하여 이를 판단할 수 있습니다. 이 호출이 오류를 반환하면 해당 객체는 임의 접근을 지원하지 않습니다. 각 구현은 자신의 타입에 맞는 메서드를 추가로 제공할 수 있습니다. ByteIO 객체는 Python 2의 cStringIO 라이브러리와 비슷하지만, 문자열 대신 새로운 bytes 타입을 다룹니다.

버퍼링된 I/O

다음 계층은 파일류 객체에 더 효율적으로 접근할 수 있게 해 주는 버퍼링된 I/O 계층입니다. 모든 버퍼링된 I/O 구현의 추상 베이스 클래스는 BufferedIOBase이며, RawIOBase와 비슷한 메서드를 제공합니다:

.read(n: int = -1) -> bytes
객체에서 다음 n 바이트를 반환합니다. 파일 끝에 도달했거나 객체가 논블로킹이면 n 바이트보다 적게 반환할 수 있습니다. 0바이트는 파일 끝을 나타냅니다. 이 메서드는 바이트를 모으기 위해 RawIOBase.read()를 여러 번 호출할 수도 있고, 필요한 바이트가 모두 이미 버퍼링되어 있다면 RawIOBase.read()를 전혀 호출하지 않을 수도 있습니다.

.readinto(b: bytes) -> int

.write(b: bytes) -> int

b 바이트를 버퍼에 씁니다. 바이트가 Raw I/O 객체에 즉시 쓰인다는 보장은 없으며, 버퍼링될 수 있습니다. len(b)를 반환합니다.

.seek(pos: int, whence: int = 0) -> int

.tell() -> int

.truncate(pos: int = None) -> int

.flush() -> None

.close() -> None

.readable() -> bool

.writable() -> bool

.seekable() -> bool

.__enter__() -> ContextManager

.__exit__(...) -> None

또한 추상 베이스 클래스는 멤버 변수 하나를 제공합니다:

.raw
하부 RawIOBase 객체에 대한 참조입니다.

BufferedIOBase 메서드 시그니처는 RawIOBase의 시그니처와 대부분 동일하지만(예외: write()는 None을 반환하고, read()의 인자는 선택 사항입니다), 의미는 다를 수 있습니다. 특히 BufferedIOBase 구현은 요청보다 더 많은 데이터를 읽거나 버퍼를 사용해 데이터 쓰기를 지연할 수 있습니다. 대부분의 경우 이는 사용자에게 드러나지 않습니다(예를 들어 같은 파일을 다른 디스크립터로 여는 경우는 예외입니다). 또한 raw 읽기는 특별한 이유 없이 요청보다 적게 읽을 수 있지만, 버퍼링된 읽기는 EOF에 도달한 경우에만 요청보다 적게 읽습니다. 그리고 raw 쓰기는 (논블로킹 I/O가 활성화되지 않은 경우에도!) 요청보다 적은 개수를 반환할 수 있지만, 버퍼링된 쓰기는 모든 바이트를 쓰거나 버퍼링하지 못하면 IOError를 발생시킵니다.

BufferedIOBase 추상 베이스 클래스에는 아래에서 설명하는 네 가지 구현이 있습니다.

BufferedReader

BufferedReader 구현은 순차 접근 방식의 읽기 전용 객체를 위한 것입니다. 이 구현의 .flush() 메서드는 아무 동작도 하지 않습니다.

BufferedWriter

BufferedWriter 구현은 순차 접근 방식의 쓰기 전용 객체를 위한 것입니다. 이 구현의 .flush() 메서드는 캐시된 모든 데이터를 하부 RawIOBase 객체에 강제로 기록합니다.

BufferedRWPair

BufferedRWPair 구현은 소켓이나 tty와 같은 순차 접근 방식의 읽기-쓰기 객체를 위한 것입니다. 이러한 객체의 읽기 스트림과 쓰기 스트림은 완전히 독립적이므로, BufferedReader 인스턴스와 BufferedWriter 인스턴스를 결합하는 것만으로 간단히 구현할 수 있습니다. 이 구현은 BufferedWriter의 .flush() 메서드와 같은 의미를 갖는 .flush() 메서드를 제공합니다.

BufferedRandom

BufferedRandom 구현은 읽기 전용이든, 쓰기 전용이든, 읽기-쓰기용이든 관계없이 모든 임의 접근 객체를 위한 것입니다. 순차 접근 객체를 다루는 앞의 클래스들과 달리, BufferedRandom 클래스는 사용자가 .seek()를 호출하여 스트림의 위치를 바꾸는 상황에 대처해야 합니다. 따라서 BufferedRandom 인스턴스는 객체 내의 논리적 위치와 실제 위치를 모두 추적해야 합니다. 이 구현은 캐시된 모든 쓰기 데이터를 하부 RawIOBase 객체에 강제로 기록하고 캐시된 모든 읽기 데이터를 버리는(이후의 읽기가 반드시 디스크에서 다시 읽어 오도록) .flush() 메서드를 제공합니다.

질문: 읽기-쓰기 객체에서 읽기와 쓰기 사이를 전환하면 .flush()가 수행된다는 것을 명세에서 의무화해야 합니까? 아니면 그것은 사용자가 의존해서는 안 되는 구현상의 편의일 뿐입니까?

읽기 전용 BufferedRandom 객체의 경우 .writable()은 False를 반환하고, .write() 메서드와 .truncate() 메서드는 IOError를 발생시킵니다.

쓰기 전용 BufferedRandom 객체의 경우 .readable()은 False를 반환하고, .read() 메서드는 IOError를 발생시킵니다.

텍스트 I/O

텍스트 I/O 계층은 스트림에서 문자열을 읽고 스트림에 문자열을 쓰는 함수를 제공합니다. 새로운 기능으로는 범용 줄바꿈과 문자 집합 인코딩 및 디코딩 등이 있습니다. 텍스트 I/O 계층은 TextIOBase 추상 베이스 클래스로 정의됩니다. 이 클래스는 BufferedIOBase 메서드와 비슷하지만 바이트 단위가 아닌 문자 단위로 동작하는 여러 메서드를 제공합니다. 이러한 메서드는 다음과 같습니다.

.read(n: int = -1) -> str

.write(s: str) -> int

.tell() -> object

현재 파일 위치를 나타내는 쿠키를 반환합니다. 이 쿠키는 whence를 0으로 설정한(즉, 절대 위치 탐색) .seek()에 사용하는 경우에만 지원됩니다.

.seek(pos: object, whence: int = 0) -> int

pos 위치로 이동합니다. pos가 0이 아니면 .tell()이 반환한 쿠키여야 하며, whence는 0이어야 합니다.

.truncate(pos: object = None) -> int

BufferedIOBase.truncate()와 같지만, pos는 (None이 아니라면) 이전에 .tell()이 반환한 쿠키여야 합니다.

원시 I/O와 달리 .seek()의 단위는 정해져 있지 않습니다 - 일부 구현(예: StringIO)은 문자를 사용하고, 다른 구현(예: TextIOWrapper)은 바이트를 사용합니다. 0을 특별히 다루는 것은 먼저 .tell()을 호출하지 않아도 스트림의 시작이나 끝으로 이동할 수 있게 하기 위한 것입니다. 구현은 .tell()이 반환하는 쿠키에 스트림 인코더 상태를 포함할 수 있습니다.

TextIOBase 구현은 하부 BufferedIOBase 객체로 그대로 전달되는 몇 가지 메서드도 제공합니다:

.flush() -> None

.close() -> None

.readable() -> bool

.writable() -> bool

.seekable() -> bool

TextIOBase 클래스 구현은 추가로 다음 메서드를 제공합니다:

.readline() -> str
줄바꿈이나 EOF가 나올 때까지 읽어 그 줄을 반환하며, 곧바로 EOF에 도달하면 ""를 반환합니다.

.__iter__() -> Iterator

파일(바로 self입니다)에서 줄을 하나씩 반환하는 이터레이터를 반환합니다.

.next() -> str

readline()과 같지만, 곧바로 EOF에 도달하면 StopIteration을 발생시킨다는 점이 다릅니다.

Python 라이브러리는 두 가지 구현을 제공할 것입니다. 주 구현인 TextIOWrapper는 버퍼링된 I/O 객체를 감쌉니다. 각 TextIOWrapper 객체에는 “.buffer”라는 프로퍼티가 있으며, 이 프로퍼티는 하부 BufferedIOBase 객체에 대한 참조를 제공합니다. 이 클래스의 초기화 메서드 시그니처는 다음과 같습니다:

.__init__(self, buffer, encoding=None, errors=None, newline=None, line_buffering=False)
buffer는 TextIOWrapper로 감쌀 BufferedIOBase 객체에 대한 참조입니다.

encoding은 바이트 표현과 문자 표현 사이의 변환에 사용할 인코딩을 가리킵니다. None이면 시스템의 로케일 설정이 기본값으로 사용됩니다.

errors는 오류 처리 방식을 나타내는 선택적 문자열입니다. encoding을 설정할 수 있는 경우라면 언제든지 이 값도 설정할 수 있습니다. 기본값은 'strict'입니다.

newline은 None, '', '\n', '\r', '\r\n' 중 하나일 수 있으며, 그 밖의 값은 모두 허용되지 않습니다. 이 인자는 줄 끝 처리 방식을 제어합니다. 동작 방식은 다음과 같습니다:

  • 입력 시 newline이 None이면 유니버설 줄바꿈 모드가 활성화됩니다. 입력의 줄은 '\n', '\r', '\r\n' 중 하나로 끝날 수 있으며, 이들은 호출자에게 반환되기 전에 '\n'으로 변환됩니다. ''이면 유니버설 줄바꿈 모드가 활성화되지만, 줄 끝은 변환되지 않은 채 호출자에게 반환됩니다. 그 밖의 유효한 값 중 하나이면 입력 줄은 주어진 문자열로만 끝나며, 줄 끝은 변환되지 않은 채 호출자에게 반환됩니다. (다시 말해, '\n'으로의 변환은 newline이 None인 경우에만 일어납니다.)
  • 출력 시 newline이 None이면, 기록되는 모든 '\n' 문자는 시스템 기본 줄 구분자인 os.linesep으로 변환됩니다. newline이 ''이면 변환이 일어나지 않습니다. newline이 그 밖의 유효한 값 중 하나이면, 기록되는 모든 '\n' 문자는 주어진 문자열로 변환됩니다. (변환 규칙이 출력과 입력에서 서로 다르다는 점에 유의하십시오.)

line_buffering이 True이면, 기록되는 문자열에 '\n' 또는 '\r' 문자가 하나 이상 포함되어 있을 때 write() 호출이 flush()를 수반하게 됩니다. 이 값은 하부 스트림이 TTY 장치임을 감지했을 때나 buffering 인자로 1이 전달되었을 때 open()이 설정합니다.

newline 매개변수에 관한 추가 참고 사항은 다음과 같습니다:

  • '\r' 줄 끝을 사용하여 파일을 생성하는 일부 OSX 애플리케이션 때문에 '\r' 지원은 여전히 필요합니다. Excel(텍스트로 내보낼 때)과 Adobe Illustrator EPS 파일이 가장 흔한 예입니다.
  • 변환이 활성화되어 있으면, 읽기나 쓰기에 어떤 메서드를 호출하든 변환이 일어납니다. 예를 들어, f.read()는 항상 ''.join(f.readlines())와 같은 결과를 냅니다.
  • 입력 시 변환 없는 유니버설 줄바꿈이 요청된 경우(즉, newline=''), 시스템 읽기 연산이 '\r'로 끝나는 버퍼를 반환하면 그 뒤에 '\n'이 오는지 확인하기 위해 시스템 읽기 연산을 한 번 더 수행합니다. 변환이 있는 유니버설 줄바꿈 모드에서는 두 번째 시스템 읽기 연산을 다음 읽기 요청 때까지 미룰 수 있으며, 이어지는 시스템 읽기 연산이 '\n'으로 시작하는 버퍼를 반환하면 그 문자는 그냥 버려집니다.

또 다른 구현인 StringIO는 하부 Buffered I/O 객체 없이 파일과 유사한 TextIO 구현을 만듭니다. BytesIO 객체를 TextIOWrapper로 감싸서 비슷한 기능을 제공할 수도 있지만, StringIO 객체는 실제로 인코딩과 디코딩을 수행할 필요가 없으므로 훨씬 더 효율적입니다. String I/O 객체는 인코딩된 문자열을 그대로 저장하기만 하면 됩니다. StringIO 객체의 __init__ 시그니처는 초깃값을 지정하는 선택적 문자열을 받으며, 초기 위치는 항상 0입니다. 인코딩이나 줄바꿈 변환은 지원하지 않으며, 기록한 문자를 항상 그대로 다시 읽게 됩니다.

유니코드 인코딩/디코딩 문제

인코딩과 오류 처리 설정을 나중에 변경할 수 있도록 해야 합니다. 유니코드 문제와 모호성(예: 발음 구별 부호, 서로게이트, 인코딩에서 유효하지 않은 바이트)에 직면했을 때 Text I/O 연산의 동작은 유니코드 encode()/decode() 메서드의 동작과 같아야 합니다. UnicodeError가 발생할 수 있습니다.

구현 참고 사항: codecs 모듈이 제공하는 기반 구조의 상당 부분을 재사용할 수 있어야 합니다. 이 모듈이 우리에게 필요한 API를 정확히 제공하지 않는다면, 바퀴를 다시 발명하지 않도록 이 모듈을 리팩터링해야 합니다.

논블로킹 I/O

논블로킹 I/O는 Raw I/O 수준에서만 완전히 지원됩니다. raw 객체가 논블로킹 모드이고 연산이 블록될 상황이면, .read()와 .readinto()는 None을 반환하고 .write()는 0을 반환합니다. 객체를 논블로킹 모드로 전환하려면 사용자가 fileno를 추출하여 직접 설정해야 합니다.

Buffered I/O 계층과 Text I/O 계층에서는 논블로킹 상태 때문에 읽기나 쓰기가 실패하면 errno가 EAGAIN으로 설정된 IOError를 발생시킵니다.

원래는 Raw I/O의 동작을 상위 계층으로 전파하는 방안을 검토했으나, 많은 예외적인 경우와 문제점이 제기되었습니다. 이러한 문제를 해결하려면 Buffered I/O 계층과 Text I/O 계층을 상당히 변경해야 했을 것입니다. 예를 들어, Buffered 논블로킹 객체에서 .flush()는 무엇을 해야 합니까? 사용자는 객체에 “버퍼에서 가능한 한 많이 쓰되, 블로킹하지는 마십시오”라고 어떻게 지시할 수 있습니까? 사용 가능한 데이터를 반드시 모두 플러시하지는 않는 논블로킹 .flush()는 직관에 어긋납니다. 이 계층들에서 논블로킹 객체와 블로킹 객체는 의미가 크게 다르기 때문에, 둘을 하나의 타입으로 통합하려는 노력은 포기하기로 합의했습니다.

open() 내장 함수

open() 내장 함수는 다음 의사 코드로 명세됩니다:

def open(filename, mode="r", buffering=None, *,
         encoding=None, errors=None, newline=None):
    assert isinstance(filename, (str, int))
    assert isinstance(mode, str)
    assert buffering is None or isinstance(buffering, int)
    assert encoding is None or isinstance(encoding, str)
    assert newline in (None, "", "\n", "\r", "\r\n")
    modes = set(mode)
    if modes - set("arwb+t") or len(mode) > len(modes):
        raise ValueError("invalid mode: %r" % mode)
    reading = "r" in modes
    writing = "w" in modes
    binary = "b" in modes
    appending = "a" in modes
    updating = "+" in modes
    text = "t" in modes or not binary
    if text and binary:
        raise ValueError("can't have text and binary mode at once")
    if reading + writing + appending > 1:
        raise ValueError("can't have read/write/append mode at once")
    if not (reading or writing or appending):
        raise ValueError("must have exactly one of read/write/append mode")
    if binary and encoding is not None:
        raise ValueError("binary modes doesn't take an encoding arg")
    if binary and errors is not None:
        raise ValueError("binary modes doesn't take an errors arg")
    if binary and newline is not None:
        raise ValueError("binary modes doesn't take a newline arg")
    # XXX Need to spec the signature for FileIO()
    raw = FileIO(filename, mode)
    line_buffering = (buffering == 1 or buffering is None and raw.isatty())
    if line_buffering or buffering is None:
        buffering = 8*1024  # International standard buffer size
        # XXX Try setting it to fstat().st_blksize
    if buffering < 0:
        raise ValueError("invalid buffering size")
    if buffering == 0:
        if binary:
            return raw
        raise ValueError("can't have unbuffered text I/O")
    if updating:
        buffer = BufferedRandom(raw, buffering)
    elif writing or appending:
        buffer = BufferedWriter(raw, buffering)
    else:
        assert reading
        buffer = BufferedReader(raw, buffering)
    if binary:
        return buffer
    assert text
    return TextIOWrapper(buffer, encoding, errors, newline, line_buffering)