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

Python 개선 제안 한국어 번역

PEP 528 – Windows 콘솔 인코딩을 UTF-8로 변경

Author:
Steve Dower <steve.dower at python.org>
Status:
Final
Type:
Standards Track
Created:
27-Aug-2016
Python-Version:
3.6
Post-History:
01-Sep-2016, 04-Sep-2016
Resolution:
Python-Dev message

Table of Contents

번역·라이선스 안내

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

초록

역사적으로 Python은 Windows 운영 체제와 상호 작용할 때 ANSI API를 사용해 왔으며, 대개 C 런타임 함수를 거쳐 사용했습니다. 그러나 이 API들은 오래전부터 권장되지 않았으며, 대신 UTF-16 API를 사용하도록 권장되어 왔습니다. 운영 체제 내부에서는 모든 텍스트가 UTF-16으로 표현되며, ANSI API는 활성 코드 페이지를 사용하여 인코딩과 디코딩을 수행합니다.

이 PEP는 Windows의 기본 표준 스트림 구현이 유니코드 API를 사용하도록 변경할 것을 제안합니다. 이를 통해 사용자는 기본 Windows 콘솔에서 모든 범위의 유니코드 문자를 출력하고 입력할 수 있게 됩니다. 이를 위해서는 토크나이저가 readline 훅에서 받은 텍스트를 구문 분석하는 방식도 미묘하게 변경해야 합니다.

구체적인 변경 사항

_io.WindowsConsoleIO 추가

현재는 표준 입력, 출력, 오류를 나타내는 파일 디스크립터를 감싸는 데 _io.FileIO의 인스턴스가 사용됩니다. Windows 콘솔 함수, 구체적으로는 ReadConsoleW와 WriteConsoleW를 사용하는 원시 IO 객체 역할을 하는 (C로 구현된) 새 클래스 _io.WindowsConsoleIO를 추가합니다.

이 클래스는 레거시 모드 플래그가 적용되지 않은 상태에서 파일 디스크립터로 표준 스트림을 열고, 그 스트림이 리디렉션된 파일이 아닌 콘솔 버퍼일 때 사용됩니다. 그 밖의 경우에는 현재와 마찬가지로 _io.FileIO가 사용됩니다.

이 클래스는 원시(바이트) IO 클래스로, 텍스트를 utf-8로 인코딩하여 전달해야 하며, 전달된 텍스트는 utf-16-le로 디코딩되어 Windows API에 전달됩니다. 마찬가지로, 이 클래스에서 읽는 바이트는 운영 체제가 utf-16-le로 제공하며, Python에 반환될 때 utf-8로 변환됩니다.

TextIOWrapper를 거치지 않고 표준 스트림에 ASCII 바이트를 직접 쓰는 코드(예를 들어 Twisted’s process_stdinreader.py)와의 호환성을 유지하려면 ASCII 호환 인코딩을 사용해야 합니다. 표준 스트림에 대해 ASCII 이외의 특정 인코딩을 가정하는 코드는 깨질 가능성이 높습니다.

_PyOS_WindowsConsoleReadline 추가

대화형 프롬프트에서 유니코드를 입력할 수 있게 하려면 새 readline 훅이 필요합니다. 기존 PyOS_StdioReadline 함수는 콘솔 버퍼인 파일 디스크립터에서 읽고 레거시 모드 플래그가 적용되지 않은 경우, 새로운 _PyOS_WindowsConsoleReadline 함수에 처리를 위임합니다(판단 로직은 위와 동일해야 합니다).

readline 인터페이스는 중간에 널 문자가 없는 8비트 인코딩 문자열을 반환해야 하므로, _PyOS_WindowsConsoleReadline 함수는 운영 체제에서 읽은 utf-16-le를 utf-8로 변환합니다.

현재 sys.stdin에서 인코딩을 가져오는 PyRun_InteractiveOneObject 함수는 레거시 모드 플래그가 적용되지 않는 한 utf-8을 선택하게 됩니다. 이로 인해 readline 훅은 인코딩을 utf-8로 변경하거나, 올바르게 동작하려면 레거시 모드를 요구해야 할 수 있습니다.

레거시 모드 추가

환경 변수 PYTHONLEGACYWINDOWSSTDIO를 설정한 상태로 Python을 실행하면 레거시 모드 플래그가 활성화되며, 이 플래그는 이전 동작을 완전히 복원합니다.

대안적 접근 방식

win_unicode_console package는 콘솔의 기본 동작을 변경하는 방식에 대한 순수 Python 대안입니다. 이 패키지는 여기에서 설명한 것과 본질적으로 동일한 수정 사항을 순수 Python 코드로 구현합니다.

깨질 수 있는 코드

다음 코드 패턴은 이 변경으로 인해 깨지거나 다르게 동작할 수 있습니다. 이 코드 예제들은 모두 눈에 보이는 변화를 막아 줄 더 편리한 래퍼 대신 원시 파일 객체를 사용하도록 명시적으로 선택해야 하는 경우입니다.

stdin/stdout 인코딩 가정

sys.stdin.buffer 또는 sys.stdout.buffer에 필요한 인코딩이 'mbcs' 또는 더 구체적인 인코딩이라고 가정하는 코드는 지금은 우연히 동작하고 있을 수 있지만, 이 변경으로 인해 문제가 생길 수 있습니다. 예를 들면 다음과 같습니다:

>>> sys.stdout.buffer.write(text.encode('mbcs'))
>>> r = sys.stdin.buffer.read(16).decode('cp437')

이 코드를 바로잡으려면 TextIOWrapper에 지정된 인코딩을 암시적으로든 명시적으로든 사용해야 합니다:

>>> # Fix 1: Use wrapper correctly
>>> sys.stdout.write(text)
>>> r = sys.stdin.read(16)

>>> # Fix 2: Use encoding explicitly
>>> sys.stdout.buffer.write(text.encode(sys.stdout.encoding))
>>> r = sys.stdin.buffer.read(16).decode(sys.stdin.encoding)

원시 객체를 잘못 사용하는 경우

원시 IO 객체를 사용하면서 부분 읽기와 부분 쓰기를 올바르게 처리하지 않는 코드는 영향을 받을 수 있습니다. 이는 특히 읽기에서 중요합니다. 입력이 훨씬 더 긴 utf-8 문자열로 인코딩되는 것을 막을 현실적인 방법이 없으므로, 읽어 들이는 문자 수는 허용된 바이트 수의 4분의 1을 결코 넘지 않습니다:

>>> raw_stdin = sys.stdin.buffer.raw
>>> data = raw_stdin.read(15)
abcdefghijklm
b'abc'
# data contains at most 3 characters, and never more than 12 bytes
# error, as "defghijklm\r\n" is passed to the interactive prompt

이 코드를 바로잡으려면 버퍼링된 reader/writer를 사용하거나, 호출자가 버퍼가 가득 찰 때까지 계속 읽어야 합니다:

>>> # Fix 1: Use the buffered reader/writer
>>> stdin = sys.stdin.buffer
>>> data = stdin.read(15)
abcedfghijklm
b'abcdefghijklm\r\n'

>>> # Fix 2: Loop until enough bytes have been read
>>> raw_stdin = sys.stdin.buffer.raw
>>> b = b''
>>> while len(b) < 15:
...     b += raw_stdin.read(15)
abcedfghijklm
b'abcdefghijklm\r\n'

작은 버퍼로 원시 객체를 사용하는 경우

원시 IO 객체를 사용하여 네 문자 미만을 읽으려고 하는 코드는 이제 오류를 받게 됩니다. 어떤 문자든 utf-8로 표현하면 최대 4바이트가 필요할 수 있으므로, 이러한 요청은 실패할 수밖에 없습니다:

>>> raw_stdin = sys.stdin.buffer.raw
>>> data = raw_stdin.read(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: must read at least 4 bytes

유일한 해결 방법은 더 큰 버퍼를 전달하는 것입니다:

>>> # Fix: Request at least four bytes
>>> raw_stdin = sys.stdin.buffer.raw
>>> data = raw_stdin.read(4)
a
b'a'
>>> >>>

(추가로 나타나는 >>>는 입력 버퍼에 남아 있는 줄바꿈 때문이며, 이 상황에서는 예상되는 동작입니다.)