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

Python 개선 제안 한국어 번역

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로 변경하고, 모든 파일 시스템 함수가 파일 시스템 경로에 Unicode API를 사용하도록 변경할 것을 제안합니다. 이는 경로를 나타내는 데 문자열을 사용하는 코드에는 영향을 주지 않지만, 경로에 바이트를 사용하는 코드는 이제 Windows 파일 시스템의 모든 유효한 경로를 올바르게 왕복 처리할 수 있게 됩니다. 현재 Unicode(OS 내)와 바이트(Python 내) 간의 변환은 손실이 발생하며 사용자의 활성 코드 페이지 외부에 있는 문자를 왕복 처리하지 못합니다.

특히 이는 파일 내용의 인코딩에는 영향을 주지 않습니다. 파일 내용은 계속해서 locale.getpreferredencoding()을 기본값으로 사용하고(텍스트 파일의 경우), 바이너리 파일의 경우에는 일반 바이트를 사용합니다. 이는 사용자가 바이트 객체를 Python에 전달하고, Python이 이를 경로 이름으로 운영 체제에 전달할 때 사용되는 인코딩에만 영향을 줍니다.

배경

파일 시스템 경로는 거의 보편적으로 파일 시스템에 의해 결정되는 인코딩을 사용하는 텍스트로 표현됩니다. Python에서는 osio 모듈과 같은 여러 인터페이스를 통해 이러한 경로를 노출합니다. 경로는 이러한 인터페이스를 통해 양방향으로 전달될 수 있습니다. 즉, 파일 시스템에서 애플리케이션으로 전달될 수도 있고(예: os.listdir()), 애플리케이션에서 파일 시스템으로 전달될 수도 있습니다(예: os.unlink()).

경로가 파일 시스템과 애플리케이션 사이에서 전달될 때, 바이트 블롭으로 그대로 전달되거나 os.fsencode()os.fsdecode()를 사용하여 str로 변환되거나, sys.getfilesystemencoding()을 사용하여 명시적으로 인코딩됩니다. sys.getfilesystemencoding()으로 문자열을 인코딩한 결과는 기본 파일 시스템의 네이티브 형식에 해당하는 바이트 블롭입니다.

Windows에서 파일 시스템의 네이티브 형식은 utf-16-le입니다. 파일 시스템에 액세스하는 데 권장되는 플랫폼 API는 모두 이 형식으로 인코딩된 텍스트를 받아들이고 반환합니다. 그러나 Windows NT 이전(그리고 어쩌면 그보다 더 이전)에는 네이티브 형식이 구성 가능한 컴퓨터 옵션이었으며, 이 형식을 받아들이는 별도의 API 집합이 존재했습니다. 이 옵션(“활성 코드 페이지”)과 이러한 API(“*A 함수”)는 하위 호환성을 위해 최신 버전의 Windows에도 여전히 존재하지만, 새로운 기능은 종종 utf-16-le API(“*W 함수”)만 제공합니다.

Python에서는 경로에 사용되는 모든 문자를 올바르게 왕복 처리할 수 있으므로 str을 권장합니다(POSIX에서는 surrogateescape 처리 방식으로, Windows에서는 str이 네이티브 표현에 매핑되므로). Windows에서 바이트는 경로에 사용되는 모든 문자를 왕복 처리할 수 없습니다. Python이 내부적으로 *A 함수를 사용하므로 인코딩이 “활성 코드 페이지가 무엇이든 간에”가 되기 때문입니다. 활성 코드 페이지는 모든 Unicode 문자를 표현할 수 없으므로, 경로를 바이트로 변환하면 경고나 이를 알릴 수 있는 방법 없이 정보가 손실될 수 있습니다.

이를 보여주는 예로:

>>> open('test\uAB00.txt', 'wb').close()
>>> import glob
>>> glob.glob('test*')
['test\uab00.txt']
>>> glob.glob(b'test*')
[b'test?.txt']

glob에 대한 두 번째 호출에서 Unicode 문자가 ‘?’로 대체되었으며, 이는 경로를 다시 파일 시스템에 전달하면 FileNotFoundError가 발생한다는 의미입니다. os.listdir() 또는 반환 형식을 매개변수 형식에 맞추는 모든 함수에서도 동일한 결과를 관찰할 수 있습니다.

사용자가 접근할 수 있는 한 가지 해결책은 어디서나 str을 사용하는 것이지만, POSIX 시스템에서는 바이트만 사용할 때 일반적으로 데이터 손실이 발생하지 않습니다. 바이트가 표준 표현이기 때문입니다. 일부 표준에 따르면 인코딩이 “잘못된” 경우에도 파일 시스템은 여전히 해당 바이트를 파일에 다시 매핑합니다. 이를 활용하면 디코딩 및 재인코딩 비용을 피할 수 있으므로(이론적으로, 그리고 POSIX에서만), b'.'를 사용하는 코드가 '.'를 사용하는 경우보다 더 빠를 수 있습니다.:

>>> for f in os.listdir(b'.'):
...     os.stat(f)
...

그 결과, POSIX 중심 라이브러리 작성자는 경로를 나타내는 데 바이트를 사용하는 것을 선호합니다. 일부 작성자에게는 코드가 이미 올바르게 인코딩된 바이트를 받을 수 있다는 점에서 편리하기도 하며, 다른 작성자는 Python 2에서 코드를 포팅하는 작업을 단순화하려고 합니다. 그러나 Unicode가 표준 표현인 Windows에서는 정확성에 대한 가정이 적용되지 않으며 오류가 발생할 수 있습니다. 이러한 잠재적인 데이터 손실 때문에 Windows에서 바이트 경로를 사용하는 것은 Python 3.3에서 더 이상 사용되지 않도록 지정되었으며, 위의 모든 코드 조각은 Windows에서 사용 중단 경고를 생성합니다.

제안

현재 기본 파일 시스템 인코딩은 ‘mbcs’이며, 이는 활성 코드 페이지를 사용하는 메타 인코더입니다. 그러나 바이트가 파일 시스템에 전달되면 *A API를 거치고 운영 체제가 인코딩을 처리합니다. 이 경우 경로는 항상 ‘mbcs:replace’에 해당하는 방식으로 인코딩되며, Python이 이를 재정의하거나 변경할 기회는 없습니다.

이 제안에서는 *A API의 모든 사용을 제거하고 *W API만 호출합니다. Windows가 str로 Python에 경로를 반환할 때 해당 경로는 utf-16-le에서 디코딩되어 텍스트로 반환됩니다(최소 표현이 무엇이든 그 표현으로). Python 코드가 경로를 bytes로 요청하면 해당 경로는 surrogatepass를 사용하여 utf-16-le에서 utf-8로 트랜스코딩됩니다(Windows는 서로게이트 쌍을 검증하지 않으므로 파일 이름에 유효하지 않은 서로게이트가 포함될 수 있습니다). 마찬가지로 경로가 bytes로 제공되면 utf-8에서 utf-16-le로 트랜스코딩되어 *W API에 전달됩니다.

utf-8의 사용은 구성할 수 없으며, 이전 동작으로 되돌리기 위한 “legacy mode” 플래그만 제공됩니다.

surrogateescape 오류 모드는 무의미한 바이트를 보존하는 것이 문제가 아니므로 여기에는 적용되지 않습니다. 운영 체제에서 반환되는 모든 경로는 유효한 Unicode인 반면, 사용자가 생성한 유효하지 않은 경로는 디코딩 오류를 발생시켜야 합니다(현재는 이러한 경로에서 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 추가

오류 모드가 이제 surrogatepassreplace 사이에서 변경될 수 있으므로, 인코딩을 수동으로 수행하는 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()를 사용합니다.

경로 변환기 업데이트

경로 변환기를 업데이트하여 바이트 또는 버퍼 객체를 항상 PyUnicode_DecodeFSDefaultAndSize()를 사용해 텍스트로 디코딩하도록 하십시오.

narrow필드를 char* 문자열에서 원래 객체가 바이트였는지를 나타내는 플래그로 변경하십시오. 이는 원래 제공된 경로와 동일한 타입으로 경로를 반환해야 하는 함수에 필요합니다.

사용하지 않는 ANSI 코드 제거

narrow필드를 사용하는 모든 코드 경로를 제거하십시오. 이제 어떤 호출자에게도 도달할 수 없기 때문입니다. 이는 posixmodule.c내부에서만 사용됩니다. 경로의 다른 사용 부분에서는 바이트 경로 사용을 디코딩으로 대체하고 *W API를 사용해야 합니다.

레거시 모드 추가

환경 변수 PYTHONLEGACYWINDOWSFSENCODING 또는 sys._enablelegacywindowsfsencoding()함수 호출로 활성화되는 레거시 모드 플래그를 추가하십시오. 함수 호출은 플래그를 활성화하는 데만 사용할 수 있으며, 프로그램은 가능한 한 초기화 시점에 가깝게 이를 사용해야 합니다. Python이 실행 중인 동안에는 레거시 모드를 비활성화할 수 없습니다.

이 플래그가 설정되면 기본 파일 시스템 인코딩은 utf-8이 아닌 mbcs로 설정되고, 오류 모드는 surrogatepass가 아닌 replace로 설정됩니다. 경로는 계속 와이드 문자로 디코딩되고 *W API만 호출되지만, Python에 전달되고 Python에서 반환되는 바이트는 이 변경 이전과 동일하게 인코딩됩니다.

Windows에서 바이트 경로 사용 비권장 해제

현재 Windows에서 바이트를 경로로 사용하는 것은 비권장됩니다. 이제 더 이상 그렇지 않으며, 경로를 바이트로 인코딩할 때는 사용자의 활성 코드 페이지가 아니라 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
  • _socket
  • subprocess
  • zipimport

다음 모듈은 sys.getfilesystemencoding()을 사용하지만 수정할 필요는 없습니다.

  • gc (이미 바이트가 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로 변경하며, mbcs는 활성 코드 페이지에 동적으로 매핑됩니다.

이 접근 방식을 사용하면 *W API로만 사용할 수 있는 새로운 기능을 사용하고 인코딩 또는 디코딩 오류도 감지할 수 있습니다. 예를 들어 유니코드 문자를 ‘?’로 조용히 대체하는 대신 작업에 대해 경고하거나 작업을 실패하게 할 수 있습니다.

제안된 수정과 비교하면 일부 새로운 기능을 사용할 수 있게 되지만, 처음에 설명한 문제 중 어느 것도 해결하지 못합니다. 라이브러리 유지 관리자가 Windows를 지원하고 파일 시스템 경로를 문자열로 처리하는 별도의 코드 경로를 추가하는 데 관심이 있다는 전제하에, 새로운 런타임 오류로 인해 일부 문제가 더 명확해지고 수정으로 이어질 수 있습니다.

인코딩을 엄격한 오류 없이 mbcs로 만드는 것은 레거시 모드 스위치가 기본적으로 활성화되는 것과 동등합니다. 실제 코드에서 상당한 손상이 발생하여 지원 중단 기간을 연장할 필요가 있지만, 그럼에도 CPython 소스의 단순화를 원하는 경우 이는 가능한 조치 방향입니다.

Windows에서 바이트 경로를 오류로 만들기

Windows에서 바이트 경로 사용을 완전히 방지함으로써 사용자가 인코딩 문제를 겪지 않도록 방지할 수 있습니다.

그러나 이 PEP의 동기는 POSIX에서 작성된 코드가 Windows에서도 올바르게 작동할 가능성을 높이는 것입니다. 이 대안은 반대 방향으로 나아가 그러한 코드를 완전히 호환되지 않게 만들 것입니다. 이는 사용자에게 어떤 식으로도 이득이 되지 않으므로, 저희는 이를 거부합니다.

모든 플랫폼에서 바이트 경로를 오류로 만들기

모든 플랫폼에서 바이트 경로 사용을 지원 중단한 후 비활성화함으로써, 코드가 원래 어디에서 작성되었는지와 관계없이 사용자가 인코딩 문제를 겪지 않도록 방지합니다. Windows 외의 플랫폼에서는 현재 경고가 전혀 없으므로, 이는 완전한 지원 중단 주기를 필요로 할 것입니다.

이는 일반적으로 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()

운영체제와 파이썬 버전 전반에서 최상의 호환성을 위해 이 코드를 수정하려면, 파일 이름을 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')

참고 문헌