PEP 529 – 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는 활성 코드 페이지를 사용하여 인코딩과 디코딩을 수행합니다. 자세한 내용은 Naming Files, Paths, and Namespaces를 참조하십시오.
이 PEP는 Windows의 기본 파일 시스템 인코딩을 utf-8로 변경하고, 모든 파일 시스템 함수가 파일 시스템 경로에 유니코드 API를 사용하도록 변경할 것을 제안합니다. 이 변경은 문자열로 경로를 표현하는 코드에는 영향을 주지 않습니다. 그러나 경로에 bytes를 사용하는 코드는 이제 Windows 파일 시스템의 모든 유효한 경로를 올바르게 왕복 변환할 수 있게 됩니다. 현재는 (OS의) 유니코드와 (Python의) bytes 사이의 변환에서 정보가 손실되며, 사용자의 활성 코드 페이지에 없는 문자는 왕복 변환에 실패합니다.
특히 이 변경은 파일 내용의 인코딩에는 영향을 주지 않습니다. 파일 내용은 계속해서 기본적으로 locale.getpreferredencoding() (텍스트 파일의 경우) 또는 일반 bytes(바이너리 파일의 경우)를 사용합니다. 이 변경은 사용자가 Python에 bytes 객체를 전달하고, 그것이 다시 운영 체제에 경로 이름으로 전달될 때 사용되는 인코딩에만 영향을 줍니다.
배경
파일 시스템 경로는 거의 예외 없이 파일 시스템이 결정하는 인코딩을 사용하는 텍스트로 표현됩니다. Python에서는 os 및 io 모듈과 같은 여러 인터페이스를 통해 이러한 경로를 노출합니다. 경로는 이 인터페이스들을 통해 양방향으로 전달될 수 있습니다. 즉, 파일 시스템에서 애플리케이션으로(예: os.listdir()), 또는 애플리케이션에서 파일 시스템으로(예: os.unlink()) 전달될 수 있습니다.
경로가 파일 시스템과 애플리케이션 사이에서 전달될 때는 bytes 덩어리 그대로 전달되거나, os.fsencode()와 os.fsdecode()를 사용하여 str로(또는 str에서) 변환되거나, sys.getfilesystemencoding()을 사용하여 명시적으로 인코딩됩니다. sys.getfilesystemencoding()으로 문자열을 인코딩한 결과는 기본 파일 시스템의 네이티브 형식으로 된 bytes 덩어리입니다.
Windows에서 파일 시스템의 네이티브 형식은 utf-16-le입니다. 파일 시스템에 접근하기 위해 권장되는 플랫폼 API는 모두 이 형식으로 인코딩된 텍스트를 받고 반환합니다. 그러나 Windows NT 이전(그리고 어쩌면 그보다 더 이전)에는 네이티브 형식이 기계별로 설정할 수 있는 옵션이었고, 이 형식을 받는 별도의 API 집합이 존재했습니다. 이 옵션(“활성 코드 페이지”)과 이 API들(”*A 함수”)은 하위 호환성을 위해 최신 버전의 Windows에도 여전히 존재하지만, 새로운 기능은 utf-16-le API(”*W 함수”)만 제공하는 경우가 많습니다.
Python에서는 str 사용을 권장합니다. str은 경로에 사용되는 모든 문자를 올바르게 왕복 변환할 수 있기 때문입니다(POSIX에서는 surrogateescape 처리를 통해 가능하고, Windows에서는 str이 네이티브 표현에 대응되기 때문에 가능합니다). Windows에서 bytes는 경로에 사용되는 모든 문자를 왕복 변환할 수 없습니다. Python이 내부적으로 *A 함수를 사용하므로 인코딩이 “활성 코드 페이지가 무엇이든 그것”이 되기 때문입니다. 활성 코드 페이지는 모든 유니코드 문자를 표현할 수 없으므로, 경로를 bytes로 변환하는 과정에서 아무런 경고나 표시 없이 정보가 손실될 수 있습니다.
이를 보여 주는 예는 다음과 같습니다:
>>> open('test\uAB00.txt', 'wb').close()
>>> import glob
>>> glob.glob('test*')
['test\uab00.txt']
>>> glob.glob(b'test*')
[b'test?.txt']
glob의 두 번째 호출에서 유니코드 문자가 ‘?’로 대체되었습니다. 이는 해당 경로를 다시 파일 시스템에 전달하면 FileNotFoundError가 발생한다는 뜻입니다. os.listdir() 또는 반환 타입을 매개변수 타입에 맞추는 모든 함수에서도 같은 결과를 볼 수 있습니다.
사용자가 적용할 수 있는 한 가지 해결책은 모든 곳에서 str을 사용하는 것입니다. 그러나 POSIX 시스템에서는 bytes가 정규 표현이므로 bytes만 사용하더라도 일반적으로 데이터 손실이 발생하지 않습니다. 어떤 기준으로 보면 인코딩이 “잘못된” 것이라 하더라도, 파일 시스템은 여전히 그 bytes를 파일에 다시 대응시킵니다. 이를 활용하면 디코딩과 재인코딩 비용을 피할 수 있습니다. 따라서 (이론상, 그리고 POSIX에서만) 다음과 같은 코드는 '.'를 사용하는 대신 b'.'를 사용하므로 더 빠를 수 있습니다:
>>> for f in os.listdir(b'.'):
... os.stat(f)
...
그 결과, POSIX 중심의 라이브러리 작성자들은 경로를 표현할 때 바이트를 사용하는 것을 선호합니다. 일부 작성자에게는 이미 올바르게 인코딩되었음이 알려진 바이트를 코드가 받을 수 있으므로 편리하기도 하며, 다른 작성자들은 Python 2 코드의 이식을 단순화하려는 것입니다. 그러나 이러한 정확성 가정은 유니코드가 표준 표현인 Windows에는 그대로 적용되지 않으며, 오류가 발생할 수 있습니다. 이러한 잠재적 데이터 손실 때문에 Windows에서 바이트 경로를 사용하는 것은 Python 3.3에서 폐지 예정(deprecated)으로 지정되었습니다. 위의 모든 코드 조각은 Windows에서 폐지 예정 경고를 발생시킵니다.
제안
현재 기본 파일 시스템 인코딩은 ‘mbcs’이며, 이는 활성 코드 페이지를 사용하는 메타 인코더입니다. 그러나 바이트가 파일 시스템에 전달되면 *A API를 거치며, 인코딩은 운영 체제가 처리합니다. 이 경우 경로는 항상 ‘mbcs:replace’에 해당하는 방식으로 인코딩되며, Python이 이를 재정의하거나 변경할 기회는 없습니다.
이 제안은 *A API의 사용을 모두 제거하고 *W API만 호출하도록 합니다. Windows가 Python에 경로를 str로 반환하면, 경로는 utf-16-le에서 디코딩되어 텍스트로(최소 표현이 무엇이든 그 형태로) 반환됩니다. Python 코드가 경로를 bytes로 요청하면, 경로는 surrogatepass를 사용하여 utf-16-le에서 utf-8로 트랜스코딩됩니다(Windows는 서로게이트 쌍을 검증하지 않으므로 파일 이름에 잘못된 서로게이트가 포함될 수 있습니다). 마찬가지로, 경로가 bytes로 제공되면 utf-8에서 utf-16-le로 트랜스코딩되어 *W API에 전달됩니다.
이전 동작으로 되돌리는 “레거시 모드” 플래그를 제공하는 것 외에는 utf-8 사용을 설정으로 변경할 수 없습니다.
무의미한 바이트를 보존하는 것이 관심사가 아니므로 surrogateescape 오류 모드는 여기에 적용되지 않습니다. 운영 체제에서 반환되는 모든 경로는 유효한 유니코드이며, 사용자가 만든 잘못된 경로는 디코딩 오류를 발생시켜야 합니다(현재는 이러한 경로가 OSError 또는 그 서브클래스를 발생시킵니다).
(utf-16-le 바이트가 아닌) utf-8 바이트를 선택한 것은 ASCII 호환 인코딩을 가정할 때 경로 이름의 왕복 변환을 보장하고 기본적인 조작(예를 들어 os.path 모듈 사용)을 가능하게 하기 위해서입니다. utf-16-le를 인코딩으로 사용하는 것이 더 순수하지만, 해결하는 문제보다 일으키는 문제가 더 많을 것입니다.
이 변경은 또한 Windows에서 바이트 경로 사용에 대한 폐지 예정 지정을 철회합니다. 바이트를 경로로 사용하는 방식의 의미 체계는 바꿀 필요가 없습니다. 이전과 마찬가지로 바이트는 sys.getfilesystemencoding()에 지정된 인코딩으로 인코딩되어야 합니다.
구체적인 변경 사항
sys.getfilesystemencoding 갱신
Py_FileSystemDefaultEncoding의 기본값을 제거하고, initfsencoding()에서 이를 utf-8로 설정하거나 레거시 모드 스위치가 활성화된 경우 mbcs로 설정합니다.
PyUnicode_DecodeFSDefaultAndSize()와 PyUnicode_EncodeFSDefault()의 구현이 utf-8 코덱을 사용하거나, 레거시 모드 스위치가 활성화된 경우 기존 mbcs 코덱을 사용하도록 갱신합니다.
sys.getfilesystemencodeerrors 추가
이제 오류 모드가 surrogatepass와 replace 사이에서 바뀔 수 있으므로, 인코딩을 직접 수행하는 Python 코드도 현재 오류 모드에 접근할 수 있어야 합니다. 여기에는 현재 코덱에 따라 오류 모드를 가정하는 os.fsencode()와 os.fsdecode()의 구현이 포함됩니다.
기존 Py_FileSystemDefaultEncoding과 유사한 공개 Py_FileSystemDefaultEncodeErrors를 추가합니다. Windows에서의 기본값은 surrogatepass이며, 레거시 모드에서는 replace입니다. 다른 모든 플랫폼에서의 기본값은 surrogateescape입니다.
현재 오류 모드를 반환하는 공개 sys.getfilesystemencodeerrors() 함수를 추가합니다.
PyUnicode_DecodeFSDefaultAndSize()와 PyUnicode_EncodeFSDefault()의 구현이 상수 문자열 대신 오류 모드 변수를 사용하도록 갱신합니다.
os.fsencode()와 os.fsdecode()의 구현이 모드를 가정하는 대신 sys.getfilesystemencodeerrors()를 사용하도록 갱신합니다.
path_converter 갱신
경로 변환기가 항상 PyUnicode_DecodeFSDefaultAndSize()를 사용하여 bytes 또는 버퍼 객체를 텍스트로 디코딩하도록 갱신합니다.
narrow 필드를 char* 문자열에서 원래 객체가 bytes였는지를 나타내는 플래그로 변경합니다. 이는 원래 제공된 것과 같은 타입으로 경로를 반환해야 하는 함수에 필요합니다.
사용하지 않는 ANSI 코드 제거
narrow 필드를 사용하는 모든 코드 경로를 제거합니다. 이러한 코드 경로에는 더 이상 어떤 호출자도 도달할 수 없기 때문입니다. 이들은 posixmodule.c 내부에서만 사용됩니다. 그 밖에 경로를 사용하는 곳에서는 bytes 경로 사용을 디코딩 및 *W API 사용으로 대체해야 합니다.
레거시 모드 추가
PYTHONLEGACYWINDOWSFSENCODING 환경 변수 또는 sys._enablelegacywindowsfsencoding() 함수 호출로 활성화되는 레거시 모드 플래그를 추가합니다. 이 함수 호출은 플래그를 활성화하는 데에만 사용할 수 있으며, 프로그램은 가능한 한 초기화 시점에 가깝게 이를 호출해야 합니다. Python이 실행되는 동안에는 레거시 모드를 비활성화할 수 없습니다.
이 플래그가 설정되면 기본 파일 시스템 인코딩은 utf-8이 아닌 mbcs로 설정되고, 오류 모드는 surrogatepass가 아닌 replace로 설정됩니다. 경로는 계속해서 와이드 문자로 디코딩되고 *W API만 호출되지만, Python에 전달되고 Python에서 받는 bytes는 이 변경 이전과 동일하게 인코딩됩니다.
Windows에서 bytes 경로의 폐지 예정 철회
현재 Windows에서 bytes를 경로로 사용하는 것은 폐지 예정(deprecated) 상태입니다. 우리는 더 이상 그렇지 않으며, bytes로 인코딩된 경로는 사용자의 활성 코드 페이지가 아니라 sys.getfilesystemencoding()이 반환하는 인코딩을 사용해야 한다고 공지할 것입니다.
베타 실험
이 변경의 영향을 판단하는 데 도움이 되도록, 3.6.0b4 이전에 최종 결정을 내린다는 의도로 이 변경을 3.6.0b1에 잠정적으로 적용할 것을 제안합니다.
실험 기간 동안에는 디코딩 및 인코딩 예외 메시지를 확장하여 현재 진행 중인 온라인 토론 링크를 포함하고 문제 보고를 장려할 것입니다.
3.6.0b4에서 이 기능을 되돌리기로 결정한다면, 구현 변경 사항은 레거시 모드 플래그를 영구적으로 활성화하고, 환경 변수를 PYTHONWINDOWSUTF8FSENCODING으로, 함수를 sys._enablewindowsutf8fsencoding()으로 변경하여, 이 기능을 비활성화하는 대신 경우에 따라 활성화할 수 있도록 하는 것입니다.
호환성 문제로 인해 3.6에서 이 변경을 현실적으로 적용할 수 없다면, 이후 Python 3.x의 어느 시점에서도 이 변경을 적용하는 것은 불가능할 것으로 예상됩니다.
영향을 받는 모듈
이 PEP는 운영 체제에 경로 이름을 전달하거나 그 밖의 방식으로 sys.getfilesystemencoding()을 사용하는 Python 내의 모든 모듈을 암묵적으로 포함합니다.
3.6.0a4 기준으로 다음 모듈은 수정이 필요합니다:
os_overlapped_socketsubprocesszipimport
다음 모듈은 sys.getfilesystemencoding()을 사용하지만 수정할 필요가 없습니다:
gc(이미 bytes가 utf-8이라고 가정합니다)grp(Windows용으로 컴파일되지 않습니다)http.server(전송되는 데이터에 코덱 이름을 올바르게 포함합니다)idlelib.editor(필요하지 않을 것이며, 대체 처리가 있습니다)nis(Windows용으로 컴파일되지 않습니다)pwd(Windows용으로 컴파일되지 않습니다)spwd(Windows용으로 컴파일되지 않습니다)_ssl(ASCII 상수에만 사용됩니다)tarfile(Windows에서는 코드가 사용되지 않습니다)_tkinter(이미 바이트가 utf-8이라고 가정합니다)wsgiref(알 수 없는 환경의 기본 인코딩으로 가정됩니다)zipapp(Windows에서는 코드가 사용되지 않습니다)
다음 네이티브 코드는 인코딩 또는 디코딩 함수 중 하나를 사용하지만, 수정할 필요는 없습니다:
Parser/parsetok.c(문서에 이미sys.getfilesystemencoding()이 명시되어 있습니다)Python/ast.c(문서에 이미sys.getfilesystemencoding()이 명시되어 있습니다)Python/compile.c(문서화되어 있지 않지만, Python 파일 시스템 인코딩이 암시됩니다)Python/errors.c(문서에 이미os.fsdecode()가 명시되어 있습니다)Python/fileutils.c(Windows에서는 코드가 사용되지 않습니다)Python/future.c(문서화되어 있지 않지만, Python 파일 시스템 인코딩이 암시됩니다)Python/import.c(문서에 이미 utf-8이 명시되어 있습니다)Python/importdl.c(Windows에서는 코드가 사용되지 않습니다)Python/pythonrun.c(문서에 이미sys.getfilesystemencoding()이 명시되어 있습니다)Python/symtable.c(문서화되어 있지 않지만, Python 파일 시스템 인코딩이 암시됩니다)Python/thread.c(Windows에서는 코드가 사용되지 않습니다)Python/traceback.c(문자열 비교를 위해 올바르게 인코딩합니다)Python/_warnings.c(문서에 이미os.fsdecode()가 명시되어 있습니다)
기각된 대안
엄격한 mbcs 디코딩 사용
이는 본질적으로 제안된 변경과 같지만, sys.getfilesystemencoding()을 utf-8로 바꾸는 대신 mbcs(활성 코드 페이지에 동적으로 매핑됩니다)로 바꿉니다.
이 접근 방식을 사용하면 *W API로만 제공되는 새로운 기능을 사용할 수 있고, 인코딩/디코딩 오류도 감지할 수 있습니다. 예를 들어, 유니코드 문자를 조용히 ‘?’로 대체하는 대신 경고를 내거나 작업을 실패시킬 수 있습니다.
제안된 수정과 비교하면, 이 방식은 몇 가지 새로운 기능을 가능하게 할 수는 있지만 처음에 설명한 문제는 하나도 해결하지 못합니다. 라이브러리 유지 관리자가 Windows를 지원하고 파일 시스템 경로를 문자열로 다루는 별도의 코드 경로를 추가하는 데 관심이 있다면, 새로운 런타임 오류로 인해 일부 문제가 더 분명하게 드러나 수정으로 이어질 수 있습니다.
인코딩을 엄격한 오류 처리 없이 mbcs로 설정하는 것은 레거시 모드 스위치를 기본으로 활성화하는 것과 같습니다. 실제 코드가 상당히 많이 깨져서 폐지 기간을 연장해야 하지만, 여전히 CPython 소스를 단순화하고 싶은 경우에 택할 수 있는 방안입니다.
Windows에서 bytes 경로를 오류로 만들기
Windows에서 bytes 경로 사용을 완전히 막으면 사용자가 인코딩 문제를 겪지 않게 됩니다.
그러나 이 PEP의 동기는 POSIX에서 작성된 코드가 Windows에서도 올바르게 동작할 가능성을 높이는 것입니다. 이 대안은 반대 방향으로 나아가 그러한 코드를 완전히 호환되지 않게 만듭니다. 이는 사용자에게 아무런 이점도 주지 않으므로 기각합니다.
모든 플랫폼에서 bytes 경로를 오류로 만들기
모든 플랫폼에서 bytes 경로 사용을 지원 중단한 뒤 비활성화하면, 코드가 원래 어디에서 작성되었는지와 관계없이 사용자가 인코딩 문제를 겪지 않게 됩니다. 현재 Windows 이외의 플랫폼에서는 경고가 전혀 없으므로, 이렇게 하려면 완전한 폐지 주기를 거쳐야 합니다.
이는 Python 개발자 전반에 대한 적대적인 조치로 받아들여질 가능성이 크므로, 현재로서는 기각합니다.
깨질 수 있는 코드
다음 코드 패턴은 이 변경으로 인해 깨지거나 다르게 동작할 수 있습니다. 이 예제들은 모두 크로스 플랫폼용 코드에서라면 원래부터 취약했을 것입니다. 제안된 수정 방법은 모든 플랫폼과 여러 Python 버전에 걸쳐 경로 인코딩 문제를 처리하는 가장 호환성 높은 방법을 보여 줍니다.
이 예제들은 모두 Python 3.3 이상에서 폐지 경고를 발생시킨다는 점에 유의하십시오.
경계를 넘을 때 인코딩을 관리하지 않는 경우
프로토콜 경계를 넘을 때 인코딩을 관리하지 않는 코드는 현재 우연히 동작하고 있을 수 있지만, 어느 한쪽의 인코딩이 바뀌면 문제가 생길 수 있습니다. 아래 두 번째 예제에서 보듯이, filename의 출처는 bytes 객체를 반환하는 어떤 함수든 될 수 있다는 점에 유의하십시오:
>>> filename = open('filename_in_mbcs.txt', 'rb').read()
>>> text = open(filename, 'r').read()
이 코드를 바로잡으려면 파일에서 읽을 때나 값을 사용하기 전에 filename에 담긴 바이트의 인코딩을 지정해야 합니다:
>>> # Fix 1: Open file as text (default encoding)
>>> filename = open('filename_in_mbcs.txt', 'r').read()
>>> text = open(filename, 'r').read()
>>> # Fix 2: Open file as text (explicit encoding)
>>> filename = open('filename_in_mbcs.txt', 'r', encoding='mbcs').read()
>>> text = open(filename, 'r').read()
>>> # Fix 3: Explicitly decode the path
>>> filename = open('filename_in_mbcs.txt', 'rb').read()
>>> text = open(filename.decode('mbcs'), 'r').read()
filename을 만드는 쪽과 filename을 사용하는 쪽이 분리되어 있다면, 인코딩은 함께 전달해야 하는 중요한 정보입니다:
>>> some_object.filename = r'C:\Users\Steve\Documents\my_file.txt'.encode('mbcs')
>>> filename = some_object.filename
>>> type(filename)
<class 'bytes'>
>>> text = open(filename, 'r').read()
운영 체제와 Python 버전 전반에서 호환성을 최대한 높이도록 이 코드를 고치려면 파일 이름을 str로 노출해야 합니다:
>>> # Fix 1: Expose as str
>>> some_object.filename = r'C:\Users\Steve\Documents\my_file.txt'
>>> filename = some_object.filename
>>> type(filename)
<class 'str'>
>>> text = open(filename, 'r').read()
또는 경로에 사용된 인코딩을 사용자가 알 수 있도록 해야 합니다. os.fsencode() (또는 sys.getfilesystemencoding())를 지정하는 것도 허용되는 선택이며, 정확한 인코딩을 담은 새 속성을 추가할 수도 있습니다:
>>> # Fix 2: Use fsencode
>>> some_object.filename = os.fsencode(r'C:\Users\Steve\Documents\my_file.txt')
>>> filename = some_object.filename
>>> type(filename)
<class 'bytes'>
>>> text = open(filename, 'r').read()
>>> # Fix 3: Expose as explicit encoding
>>> some_object.filename = r'C:\Users\Steve\Documents\my_file.txt'.encode('cp437')
>>> some_object.filename_encoding = 'cp437'
>>> filename = some_object.filename
>>> type(filename)
<class 'bytes'>
>>> filename = filename.decode(some_object.filename_encoding)
>>> type(filename)
<class 'str'>
>>> text = open(filename, 'r').read()
명시적으로 ‘mbcs’를 사용하는 경우
파일 시스템 API에 전달하기 전에 ‘mbcs’를 사용하여 텍스트를 명시적으로 인코딩하는 코드는 이제 잘못 인코딩된 바이트를 전달하게 됩니다. 이 예제에서 filename이 str이기만 하면 그 출처는 상관없다는 점에 유의하십시오:
>>> filename = open('files.txt', 'r').readline().rstrip()
>>> text = open(filename.encode('mbcs'), 'r')
이 코드를 바로잡으려면 문자열을 명시적인 인코딩 없이 전달하거나 os.fsencode()를 사용해야 합니다:
>>> # Fix 1: Do not encode the string
>>> filename = open('files.txt', 'r').readline().rstrip()
>>> text = open(filename, 'r')
>>> # Fix 2: Use correct encoding
>>> filename = open('files.txt', 'r').readline().rstrip()
>>> text = open(os.fsencode(filename), 'r')
참고 문헌
Copyright
This document has been placed in the public domain.