PEP 538 – 레거시 C 로케일을 UTF-8 기반 로케일로 강제 변환합니다
- Author:
- Alyssa Coghlan <ncoghlan at gmail.com>
- BDFL-Delegate:
- INADA Naoki
- Status:
- Final
- Type:
- Standards Track
- Created:
- 28-Dec-2016
- Python-Version:
- 3.7
- Post-History:
- 03-Jan-2017, 07-Jan-2017, 05-Mar-2017, 09-May-2017
- Resolution:
- Python-Dev message
Table of Contents
- 초록
- 구현 참고 사항
- 배경
- 다른 PEP와의 관계
- 동기
- 설계 원칙
- 사양
- 플랫폼 지원 변경 사항
- 근거
- C 로케일 처리 개선
- 표준 IO 스트림에서 “surrogateescape” 오류 처리를 기본값으로 사용합니다.
- UTF-8 로캘 강제 변환 중 PYTHONIOENCODING 설정을 피합니다.
- 레거시 C 로캘에서 ASCII 기반 텍스트 처리에 대한 공식 지원을 중단합니다.
- 독립 실행형으로 실행할 때만 암묵적인 로캘 강제 변환을 제공합니다.
- 레거시 동작 복원 허용
- C 로캘 감지를 위한 LC_CTYPE 조회
- UTF-8 로캘 강제 변환을 위한 LC_CTYPE 명시적 설정
- UTF-8 로캘 강제 변환 시 LANG 설정 방지
- UTF-8 로캘 강제 변환 시 LC_ALL 설정 방지
- 현재 환경에 LC_ALL이 설정된 경우 로캘 강제 변환 건너뛰기
- “UTF-8 모드”와 독립적으로 로캘 강제 변환 고려
- Mac OS X, iOS 및 Android에서 C 로케일 강제와 경고 활성화
- 구현
- 이전 Python 3 릴리스로 백포팅
- 감사의 말
- 참조
- Copyright
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
Python 3의 *nix 시스템에서 지속적으로 발생하는 문제는 동일한 프로세스 또는 하위 프로세스의 다른 로케일 인식 구성 요소와의 일관성을 위해 기본적으로 구성된 로케일 인코딩을 사용해야 한다는 점과, POSIX:2001에 정의된 표준 C 로케일이 일반적으로 ASCII를 기본 텍스트 인코딩으로 사용한다는 점 사이의 충돌입니다. 다언어 환경에서 네트워크 서비스와 클라이언트 애플리케이션을 개발하기에는 ASCII가 전혀 적합하지 않습니다.
PEP 540은 레거시 C 로케일을 CPython이 처리하는 방식을 변경하여, 이러한 환경에서 CPython이 운영 체제 인터페이스와 통신하는 데 ASCII를 적절한 인코딩으로 간주하는 명백히 문제가 있는 가정을 고수하는 대신 UTF-8을 사용한다고 가정하도록 제안합니다. 이는 네트워크 인코딩 상호 운용성이 로컬 인코딩 상호 운용성보다 중요한 경우에 적합한 접근 방식입니다.
그러나 그 대가로 CPython의 인코딩 가정이 동일한 프로세스의 다른 로케일 인식 구성 요소 및 동일한 환경을 공유하는 하위 프로세스에서 실행되는 구성 요소의 가정과 달라집니다.
이로 인해 일부 확장 모듈(예: GNU readline의 명령줄 기록 편집)과 하위 프로세스에서 실행되는 구성 요소(예: 이전 Python 런타임)에서 상호 운용성 문제가 발생할 수 있습니다.
또한 Python 3.7 이전의 Python 버전에서 지원되는 기존 구성 설정에 주로 의존하는 대신 CPython 자체의 내부 작동 방식을 상당히 변경해야 합니다.
따라서 이 PEP에서는 PEP 540에서 제안한 UTF-8 모드와 독립적으로, CPython 구현이 기본 C 로케일을 처리하는 방식을 다음의 기존 구성 설정(Python 3.1부터 지원됨)과 대략 동등하게 변경할 것을 제안합니다:
LC_CTYPE=C.UTF-8
PYTHONIOENCODING=utf-8:surrogateescape
강제 변환의 정확한 대상 로케일은 실제로 사용 가능한 로케일을 기반으로 런타임에 미리 정의된 목록에서 선택됩니다.
재해석된 로케일 설정은 동일한 프로세스와 하위 프로세스의 다른 구성 요소에서 확인할 수 있도록 환경에 다시 기록되지만, PYTHONIOENCODING기본값 변경은 surrogateescape오류 처리기를 제공하지 않는 Python 2 하위 프로세스와의 호환성 문제를 방지하기 위해 암묵적으로 처리됩니다.
새로운 레거시 로케일 강제 변환 동작은 LC_ALL을 설정하거나(이 경우에도 유니코드 호환성 경고가 발생할 수 있음), 새로운 PYTHONCOERCECLOCALE환경 변수를 0으로 설정하여 비활성화할 수 있습니다.
이 변경으로 인해 표준 구성의 일부로 C.UTF-8, C.utf8 또는 UTF-8 로케일 중 하나 이상을 제공하지 않는 모든 *nix 플랫폼은 적절한 로케일을 기본 C 로케일이 아닌 것으로 명시적으로 구성한 경우에만 CPython 3.7 이상 배포에서 완전히 지원되는 플랫폼으로 간주됩니다(예: en_AU.UTF-8, zh_CN.gb18030). 이 PEP와 함께 PEP 540이 채택되면 제안된 PYTHONUTF8모드를 사용할 때 순수 Python 모듈도 지원되지만, 확장 모듈의 완전한 유니코드 호환성에 대한 기대는 이 PEP에서 다루는 플랫폼으로 계속 제한됩니다.
이는 근본적으로 새로운 기능이 아니라 기본 설정의 변경만을 반영하므로, 상위 CPython 개발 팀보다 대상 사용자가 더 제한적인 재배포자(예: Linux 배포판)는 필요한 변경 사항을 다운스트림 패치로 적용하여 Python 3.6.x 계열에서 이 로케일 강제 변환 동작을 선택적으로 활성화할 수도 있습니다.
구현 참고 사항
원래 승인된 대로 PEP를 구현하려고 시도한 결과, 기본적으로 로케일 강제 변환 및 호환성 경고를 출력하자는 제안은 현실적으로 실행하기 어렵다는 사실이 드러났습니다. 영향을 받은 코드에 잠재된 로케일 처리 결함 때문이 아니라 경고 때문에 이전에 정상적으로 작동하던 코드가 실패하는 경우가 너무 많았기 때문입니다.
그 결과 PY_WARN_ON_C_LOCALE구성 플래그가 제거되고, 런타임 PYTHONCOERCECLOCALE=warn환경 변수 설정으로 대체되었습니다. 이를 통해 개발자와 시스템 통합자는 기본적으로 경고를 출력하지 않으면서 로케일 강제 변환 및 호환성 경고를 받도록 선택할 수 있습니다.
PEP 자체의 출력 예제도 경고를 제거하고 더 쉽게 읽을 수 있도록 업데이트되었습니다.
배경
CPython 인터프리터가 시작되는 동안 전체 시스템의 로케일 설정과 일관된 방식으로 char *형식에서 wchar_t *형식으로, 또는 이러한 형식 중 하나에서 PyUnicodeObject *로 변환해야 할 수 있습니다. CPython은 운영 체제에 변환을 수행하도록 의존한 다음, sys.getfilesystemencoding()이 보고하는 텍스트 인코딩 이름이 이 초기 부트스트랩 과정에서 사용된 인코딩과 일치하도록 보장하여 이러한 경우를 처리합니다.
Windows에서는 이러한 변환에 기본적으로 사용되는 mbcs형식의 한계가 충분히 문제가 되었으므로, 운영 체제가 제공하는 바이너리 데이터 처리 인터페이스를 우회하고 대신 UTF-8을 강제로 사용하도록 PEP 528과 PEP 529가 구현되었습니다.
Mac OS X, iOS 및 Android에서는 CPython을 포함한 많은 구성 요소가 로케일 설정과 관계없이 이미 UTF-8을 시스템 인코딩으로 간주합니다. 그러나 모든 구성 요소가 그런 것은 아니며, 이러한 불일치로 인해 일부 상황에서 문제가 발생할 수 있습니다(예: GNU readline 모듈 [16]을 사용할 때).
Apple 및 Android가 아닌 *nix 시스템에서는 이러한 작업이 glibc의 C 로케일 시스템을 사용하여 처리되며, 이 시스템에는 다음과 같은 특성이 있습니다 [4]:
- 기본적으로 모든 프로세스는 이러한 변환에
ASCII를 사용하는C로케일에서 시작합니다. 이는 다언어 텍스트 처리를 수행하는 사람이 실제로 원하는 경우가 거의 없습니다(CPython 및 C/C++ GUI 프레임워크 포함). setlocale(LC_ALL, "")을 호출하면 현재 프로세스 환경에 구성된 로케일 범주를 기반으로 활성 로케일이 재구성됩니다.- 현재 환경에서 요청한 로케일을 알 수 없거나 특정 로케일이 구성되지 않은 경우 기본
C로케일이 계속 활성 상태로 유지됩니다.
CPython이 의존하는 API를 포괄하는 특정 로케일 범주는 LC_CTYPE이며, 이는 “문자의 분류 및 변환과 멀티바이트 문자 및 와이드 문자”에 적용됩니다 [5]. 따라서 CPython에는 다음과 같은 setlocale에 대한 주요 호출이 포함되어 있습니다:
- 주
python바이너리에서 CPython은 프로세스 환경에 따라 전체 C 로캘 하위 시스템을 구성하도록setlocale(LC_ALL, "")을 호출합니다. 이는 공유 CPython 라이브러리에 대한 호출을 수행하기 전에 이루어집니다. Py_Initialize에서 CPython은 해당 범주의 구성된 로캘 설정이 환경에 설정된 값과 항상 일치하도록setlocale(LC_CTYPE, "")을 호출합니다. 이는 무조건 수행되며,Py_Finalize에서 프로세스 상태 변경을 되돌리지 않습니다.
(로캘 처리에 대한 이 요약에서는 로캘 설정의 일부로 선언된 텍스트 인코딩이 정확히 어디에서 언제 사용되는지와 관련된 몇 가지 기술적 세부 사항을 생략합니다. 자세한 내용은 PEP 540을 참조하십시오. 이러한 특정 세부 사항은 UTF-8 기반 로캘로 로캘을 재정의할 때보다 선언된 C 로캘에서 CPython을 분리할 때 더 중요합니다.)
이러한 호출은 일반적으로 합리적인 동작을 제공하기에 충분하지만, 다음과 같은 경우에는 여전히 실패할 수 있습니다.
- SSH 환경 전달로 인해 SSH 클라이언트가 해당 로캘이 설치되어 있지 않은 서버에 클라이언트 로캘 설정을 전달하는 경우가 있습니다. 이로 인해 CPython이 기본 ASCII 기반 C 로캘에서 실행됩니다.
- 일부 프로세스 환경(예: Linux 컨테이너)에는 명시적으로 구성된 로캘이 전혀 없을 수 있습니다. 알 수 없는 로캘의 경우와 마찬가지로, 이로 인해 CPython이 기본 ASCII 기반 C 로캘에서 실행됩니다.
- Android에서는 환경 변수에 따라 로캘을 구성하는 대신 빈 로캘
""이"C"로캘을 특별히 요청한 것으로 처리됩니다.
현재 출시된 CPython 버전에서 이 문제를 처리하는 가장 간단한 방법은 애플리케이션을 시작할 때 더 합리적인 로캘을 명시적으로 설정하는 것입니다. 예를 들면 다음과 같습니다.:
LC_CTYPE=C.UTF-8 python3 ...
C.UTF-8 로캘은 LC_CTYPE범주에 UTF-8을 사용하고, 다른 모든 범주(LC_COLLATE 포함)에는 C 로캘과 동일한 설정을 사용하는 완전한 로캘 정의입니다. 이는 ASCII 기반 C 로캘의 대안으로 여러 Linux 배포판(Debian, Ubuntu, Fedora, Alpine 및 Android 포함)에서 제공됩니다. 일부 다른 플랫폼(예: HP-UX)에서는 C.utf8이라는 이름으로 동등한 로캘 정의를 제공합니다.
Mac OS X 및 기타 *BSD 시스템은 다른 접근 방식을 취했습니다. C.UTF-8 로캘을 제공하는 대신 LC_CTYPE 범주만 정의하는 부분적인 UTF-8 로캘을 제공합니다. 이러한 시스템에서 선호되는 환경 로캘 조정 방법은 LC_ALL이나 LANG을 설정하는 대신 LC_CTYPE=UTF-8을 설정하는 것입니다. [17]
Docker 컨테이너 및 유사한 기술의 경우에는 컨테이너 이미지 정의에서 적절한 로캘 설정을 직접 지정할 수 있습니다.
또 다른 일반적인 실패 사례는 개발자가 더 좁은 범위의 LC_MESSAGES=C이나 LANGUAGE=en 대신, 번역되어 있지 않은 사용자 인터페이스 메시지를 영어로 보기 위해 LANG=C를 지정하는 것입니다.
다른 PEP와의 관계
이 PEP는 PEP 540(기본 C 로캘에서 Python 3의 동작 개선)와 공통된 문제를 다루지만, 제안된 해결책에서는 크게 달라집니다.
- PEP 540은 이 경우 CPython의 기본 텍스트 인코딩을 C 로캘 시스템에서 완전히 분리하여, 동일한 프로세스와 하위 프로세스에서 실행되는 CPython과 다른 로캘 인식 구성 요소 사이에 텍스트 처리 불일치가 발생하도록 허용할 것을 제안합니다. 이 접근 방식은 CPython이 로캘 인식 애플리케이션보다는 로캘 독립적인 언어 런타임(예: Go, Node.js(V8) 및 Rust용 런타임)과 더욱 유사하게 동작하도록 하는 것을 목표로 합니다.
- 이 PEP는 레거시 C 로캘을 UTF-8을 기본 텍스트 인코딩으로 사용하는 보다 최근에 정의된 로캘로 재정의할 것을 제안합니다. 즉, 텍스트 인코딩 재정의가 CPython뿐만 아니라 현재 프로세스에 로드된 모든 로캘 인식 확장 모듈과, 부모 프로세스에서 환경을 상속하는 하위 프로세스에서 호출된 로캘 인식 애플리케이션에도 적용됩니다. 이 접근 방식은 다른 로캘 인식 구성 요소와의 통합에 대한 CPython의 전통적인 강력한 지원을 유지하는 동시에, 더 넓은 C/C++ 생태계에서 레거시 C 로캘을 유니코드를 인식하는 대체 수단으로 사용할 C.UTF-8 로캘의 채택과 표준화를 적극적으로 촉진하는 것을 목표로 합니다.
두 PEP를 검토한 후 기술적 수준에서는 실제로 서로 충돌하지 않는다는 점이 분명해졌으며, 적합한 로캘을 사용할 수 없는 경우에는 PEP 540의 제안이 더 우수한 선택지를 제공하고, “로캘 인코딩”이라는 개념이 의미가 없는 플랫폼(예를 들어 CPython 참조 인터프리터가 아닌 MicroPython을 실행하는 임베디드 시스템)에서는 더 나은 참조 동작을 제공한다는 점도 분명해졌습니다.
한편, 이 PEP는 다른 로캘 인식 구성 요소와의 호환성을 향상했으며, 하위 배포자가 Python 3.6으로 백포트하기에 더 적합한 접근 방식을 제공했습니다.
그 결과 이 PEP는 PEP 540을 표준 UTF-8 기반 로캘을 사용할 수 없을 때 동작을 개선하는 상호 보완적 해결책으로 언급하도록 수정되었으며, 현재 독립적으로 구성할 수 없는 API(예: open()의 기본 인코딩 및 오류 처리기)까지 기본 설정의 변경 사항을 확장하도록 수정되었습니다.
또한 PEP 540의 사용 가능성으로 인해 LC_CTYPE=en_US.UTF-8 레거시 대체 수단이 강제 변환 대상으로 시도되는 UTF-8 로케일 목록에서 제거되었으며, 이러한 경우 CPython은 제안된 PYTHONUTF8 모드에만 의존할 것으로 예상됩니다.
동기
Docker, Kubernetes, OpenShift와 같은 리눅스 컨테이너 기술은 웹 서비스 개발에 사용하는 것으로 가장 잘 알려져 있지만, 관련 컨테이너 형식과 실행 모델은 리눅스 명령줄 애플리케이션 개발에도 도입되고 있습니다. Gnome Flatpak [7] 및 Ubuntu Snappy [8]와 같은 기술은 이러한 동일한 기법을 리눅스 GUI 애플리케이션 개발에도 적용하는 것을 추가로 목표로 합니다.
이러한 환경에서 Python 3를 사용하여 애플리케이션을 개발할 때 다음과 유사한 텍스트 인코딩 관련 오류를 보는 일은 드물지 않습니다.:
$ docker run --rm fedora:25 python3 -c 'print("ℙƴ☂ℌøἤ")'
Unable to decode the command from the command line:
UnicodeEncodeError: 'utf-8' codec can't encode character '\udce2' in position 7: surrogates not allowed
$ docker run --rm ncoghlan/debian-python python3 -c 'print("ℙƴ☂ℌøἤ")'
Unable to decode the command from the command line:
UnicodeEncodeError: 'utf-8' codec can't encode character '\udce2' in position 7: surrogates not allowed
동일한 명령을 로컬에서 실행하면 정상적으로 작동할 가능성이 높은데도 그렇습니다.:
$ python3 -c 'print("ℙƴ☂ℌøἤ")'
ℙƴ☂ℌøἤ
대신 세 환경에서 locale 명령을 실행하면 문제의 원인을 확인할 수 있습니다.:
$ locale | grep -E 'LC_ALL|LC_CTYPE|LANG'
LANG=en_AU.UTF-8
LC_CTYPE="en_AU.UTF-8"
LC_ALL=
$ docker run --rm fedora:25 locale | grep -E 'LC_ALL|LC_CTYPE|LANG'
LANG=
LC_CTYPE="POSIX"
LC_ALL=
$ docker run --rm ncoghlan/debian-python locale | grep -E 'LC_ALL|LC_CTYPE|LANG'
LANG=
LANGUAGE=
LC_CTYPE="POSIX"
LC_ALL=
이 특정 예에서는 호스트 시스템 로케일이 “en_AU.UTF-8”로 설정되어 있으므로 CPython이 UTF-8을 기본 텍스트 인코딩으로 사용한다는 것을 알 수 있습니다. 반면 Fedora와 Debian의 기본 Docker 이미지는 특정 로케일이 설정되어 있지 않으므로 기본적으로 POSIX 로케일을 사용하며, 이는 ASCII 기반 기본 C 로케일의 별칭입니다.
정확한 버전과 관계없이 Python 3가 Fedora 및 Debian 기반 컨테이너에서 합리적으로 동작하게 하는 가장 간단한 방법은 두 배포판이 모두 제공하는 C.UTF-8 로케일에서 실행하는 것입니다.:
$ docker run --rm -e LC_CTYPE=C.UTF-8 fedora:25 python3 -c 'print("ℙƴ☂ℌøἤ")'
ℙƴ☂ℌøἤ
$ docker run --rm -e LC_CTYPE=C.UTF-8 ncoghlan/debian-python python3 -c 'print("ℙƴ☂ℌøἤ")'
ℙƴ☂ℌøἤ
$ docker run --rm -e LC_CTYPE=C.UTF-8 fedora:25 locale | grep -E 'LC_ALL|LC_CTYPE|LANG'
LANG=
LC_CTYPE=C.UTF-8
LC_ALL=
$ docker run --rm -e LC_CTYPE=C.UTF-8 ncoghlan/debian-python locale | grep -E 'LC_ALL|LC_CTYPE|LANG'
LANG=
LANGUAGE=
LC_CTYPE=C.UTF-8
LC_ALL=
Docker, Inc.에서 제공하는 Alpine Linux 기반 Python 이미지는 이미 기본적으로 C.UTF-8 로케일을 사용합니다.:
$ docker run --rm python:3 python3 -c 'print("ℙƴ☂ℌøἤ")'
ℙƴ☂ℌøἤ
$ docker run --rm python:3 locale | grep -E 'LC_ALL|LC_CTYPE|LANG'
LANG=C.UTF-8
LANGUAGE=
LC_CTYPE="C.UTF-8"
LC_ALL=
마찬가지로 사용자 지정 컨테이너 이미지, 즉 기본 배포판 이미지 위에 추가 콘텐츠를 추가하는 이미지의 경우 이미지 정의에서 더 적합한 로케일을 설정하여 기본적으로 모든 것이 바로 작동하게 할 수 있습니다. 그러나 재배포자나 최종 사용자가 시스템 구성 변경을 통해 이 문제를 처리하도록 의존하기보다는 CPython이 이 문제를 자동으로 처리할 수 있다면 훨씬 더 쾌적하고 일관된 사용자 경험을 제공할 수 있습니다.
glibc 개발자들이 CPython [6]과 같은 glibc 기반 애플리케이션에서 사용할 수 있도록 C.UTF-8 로케일을 보편적으로 제공하기 위해 노력하고 있지만, 안타깝게도 이는 해당 기능이 없는 이전 버전의 glibc를 제공하면서 Debian과 Fedora처럼 C.UTF-8 또는 그에 상응하는 로케일을 디스크상의 로케일로 제공하지 않는 플랫폼에는 도움이 되지 않습니다. 이러한 플랫폼은 이 PEP의 범위 밖에 있는 것으로 간주합니다. 가능한 환경에서 CPython의 기본 동작을 개선하기 위한 선택지에 관한 추가 논의는 PEP 540을 참조하십시오.
설계 원칙
위의 동기에 따라 제안된 해결책은 다음과 같은 핵심 설계 원칙을 따릅니다.
- 기본 C 로케일이 아닌 로케일이 명시적으로 구성된 경우에는 계속 이를 존중합니다.
- 가능한 한 변경 사항에는 existing 구성 옵션을 사용합니다.
- 잠재적인 강제 변환 대상 로케일에서 Python의 런타임 동작은 로케일이 환경에서 명시적으로 설정되었는지, 아니면 로케일 강제 변환 대상으로 암묵적으로 설정되었는지와 관계없이 동일해야 합니다.
- Python 3.7에서는 명시적인 구성 옵션 없이 로케일 설정을 변경하는 경우 프로세스 구성을 조용히 변경하는 대신 그렇게 하고 있다는 경고를 표준 오류(stderr)에 출력합니다. 사용자가 PEP 절차나 Python 릴리스 공지를 면밀히 따르지 않더라도 애플리케이션 및 시스템 통합자에게 변경 사항을 알릴 수 있습니다. 그러나 최종 사용자를 위한 새로운 문제가 발생할 가능성을 최소화하기 위해, 경고 시스템을 사용하지 않고 이 작업을 수행하므로
-Werror를 사용하여 실행하더라도 런타임 예외로 변환되지 않습니다. (참고: 이러한 경고는 결국 기본적으로 억제되었습니다. 자세한 내용은 위의 구현 참고 사항을 참조하십시오.) - Python 3.7에서는 변경된 기본값에 빌드 시점, 런타임 또는 양쪽 모두에서 어떤 형태로든 명시적인 “off” 스위치를 제공합니다.
현재 GB-18030 또는 부분적으로 ASCII와 호환되는 다른 범용 인코딩을 사용하도록 올바르게 구성된 시스템에 대한 부정적인 영향을 최소화하려면 다음과 같은 설계 원칙이 필요합니다.
- UTF-8 기반 리눅스 컨테이너가 비UTF-8 인코딩을 사용하도록 명시적으로 구성된 호스트에서 실행되고, 명시적으로 UTF-8로 인코딩된 데이터를 교환하는 대신 해당 호스트와 로컬 인코딩 데이터를 교환하려는 경우, CPython은 일반적인 ASCII 호환 코드 포인트에서만 연결되거나 분할된 호스트 제공 데이터를 올바르게 왕복 변환하도록 노력하지만, 그 외의 경우에는 무의미한 결과를 출력할 수 있습니다.
그 밖에는 레거시 C 로케일에서 실행하면서 LC_TIME, LC_MONETARY 또는 LC_NUMERIC와 같은 명시적인 로케일 범주를 사용하도록 올바르게 구성된 시스템과 프로그램에 대한 부정적인 영향을 최소화하려면 다음과 같은 설계 원칙이 필요합니다.
LC_CTYPE이외의 로케일 범주에 대한 기존 설정을 변경할 수 있는 환경 변경을 수행하지 마십시오. (특히LC_ALL또는LANG을 설정하지 마십시오.)
마지막으로 오케스트레이션 사용 사례에서 임의의 하위 프로세스를 실행할 때의 호환성을 유지하려면 다음과 같은 설계 원칙이 필요합니다.
- 아직 지원되는 CPython 버전(예: CPython 2.7)과 호환되지 않을 수 있는 Python 관련 환경 변경을 수행하지 마십시오.
사양
CPython이 그렇지 않으면 C 로케일에서 작동하려고 시도하게 되는 경우를 더 잘 처리하기 위해, 이 PEP에서는 CPython이 독립 실행형 명령줄 애플리케이션으로 실행될 때 LC_CTYPE 범주에 대한 레거시 C 로케일을 UTF-8 기반 로케일로 자동 강제 변환하도록 시도할 것을 제안합니다.
또한 언어 런타임 자체가 초기화되는 시점에 LC_CTYPE 범주에서 레거시 C 로케일이 적용되어 있고 로케일 강제 변환을 비활성화하는 명시적 환경 플래그가 설정되지 않은 경우, 시스템 및 애플리케이션 통합자에게 지원되지 않는 구성에서 CPython을 실행하고 있음을 알리기 위해 표준 오류(stderr)에 경고를 출력할 것을 제안합니다.
이러한 일반적인 변경 사항에 더해, 해당 플랫폼에서 setlocale의 동작 차이를 처리하기 위한 Android별 추가 변경 사항도 제안됩니다.
독립 실행형 Python 인터프리터 바이너리에서의 레거시 C 로캘 강제 변환
독립 실행형 애플리케이션으로 실행될 때 CPython은 프로세스에서 로캘 종속 작업이 실행되기 전에 C 로캘을 재구성할 수 있습니다.
즉, CPython 런타임뿐만 아니라 현재 프로세스에서 실행 중인 다른 로캘 인식 구성 요소(예: 확장 모듈의 일부)와 현재 프로세스에서 환경을 상속하는 하위 프로세스의 로캘 설정도 변경할 수 있습니다.
현재 프로세스의 로캘 설정을 초기화하기 위해 setlocale(LC_ALL, "")을 호출한 후, 주 인터프리터 바이너리는 다음 호출을 포함하도록 업데이트됩니다.:
const char *ctype_loc = setlocale(LC_CTYPE, NULL);
이 난해한 호출은 현재 로캘 설정을 변경하지 않고 조회하기 위해 C가 제공하는 API입니다. 이 조회 결과를 사용하면 strcmp로 정확히 C 로캘인지 확인할 수 있습니다.:
ctype_loc != NULL && strcmp(ctype_loc, "C") == 0 # true only in the C locale
특정 로캘이 설정되지 않았거나 명목상의 로캘이 C 로캘의 별칭(예: POSIX)으로 설정된 경우에도 이 호출은 "C" 를 반환합니다.
이 정보를 바탕으로 CPython은 ASCII 대신 UTF-8을 기본 인코딩으로 사용하는 로캘로 로캘을 강제 변환하려고 시도할 수 있습니다.
다음 세 로캘이 시도됩니다.
C.UTF-8(Debian, Ubuntu, Alpine 및 Fedora 25 이상에서 최소한 사용할 수 있으며, 향후 glibc 버전에서는 기본적으로 사용할 수 있을 것으로 예상됩니다.)C.utf8(최소한 HP-UX에서 사용할 수 있습니다.)UTF-8(Mac OS X를 포함한 일부 *BSD 변형에서 최소한 사용할 수 있습니다.)
강제 변환은 LC_CTYPE 환경 변수를 후보 로캘 이름으로 설정하는 방식으로 구현되며, 이에 따라 이후의 setlocale() 호출과 해당 설정을 찾는 다른 구성 요소(예: GUI 개발 프레임워크 및 Python 자체의 locale 모듈)가 이를 확인할 수 있습니다.
플랫폼 간 바이너리 이식성을 높이고 향후 로캘 사용 가능성의 변화에 자동으로 대응하기 위해, 어떤 로캘을 시도할지 컴파일 시점에 결정하는 대신 Windows 이외의 모든 플랫폼에서 이러한 검사를 런타임에 수행합니다.
이 로캘 강제 변환이 활성화되면 다음 경고가 stderr에 출력되며, 경고에는 성공적으로 구성된 로캘이 포함됩니다.:
Python detected LC_CTYPE=C: LC_CTYPE coerced to C.UTF-8 (set another
locale or PYTHONCOERCECLOCALE=0 to disable this locale coercion behaviour).
(참고: 이 경고는 결국 기본적으로 표시되지 않도록 억제되었습니다. 자세한 내용은 위의 Implementation Note를 참조하십시오.)
현재 플랫폼이 후보 UTF-8 기반 환경 중 하나 이상을 제공하는 한, 이 로캘 강제 변환을 통해 표준 Python 바이너리 and 로캘 인식 확장 모듈이 우리가 알고 있는 세 가지 주요 실패 사례(로캘 설정 누락, LANG 또는 LC_CTYPE 를 통한 알 수 없는 로캘의 SSH 전달, 개발자가 LANG=C 를 명시적으로 요청하는 경우)에서 다시 “그냥 작동”해야 합니다.
여전히 실패할 수 있는 한 가지 경우는 stderr에 출력이 없는지 특별히 확인하는 경우이며, 이는 C 로캘 이외의 로캘을 구성하거나 하위 프로세스 오류를 확인할 때 “stderr에 출력이 없었다”는 방법 이외의 메커니즘(예: 프로세스 반환 코드 확인)을 사용하면 해결할 수 있습니다.
후보 로캘 중 어느 것도 성공적으로 구성되지 않았거나 현재 프로세스 환경에 LC_ALL 로캘 재정의가 정의되어 있으면, 초기화는 C 로캘에서 계속되며 다음 절에서 설명하는 유니코드 호환성 경고가 다른 애플리케이션에서와 마찬가지로 표시됩니다.
PYTHONCOERCECLOCALE=0이 명시적으로 설정되면, 초기화는 C 로캘에서 계속되고 다음 절에서 설명하는 유니코드 호환성 경고는 자동으로 억제됩니다.
인터프리터는 시작 시 항상 PYTHONCOERCECLOCALE환경 변수를 확인합니다(-E 또는 -I 스위치로 실행할 때도 마찬가지입니다). 로캘 강제 변환 검사는 반드시 명령줄 인자 처리 전에 수행되기 때문입니다. 일관성을 위해 로캘 호환성 경고를 억제할지 여부를 결정하는 런타임 검사도 이 설정과 독립적으로 수행됩니다.
런타임 초기화 중 레거시 C 로캘 경고
Py_Initialize 가 호출될 때가 되면 현재 프로세스에서 임의의 로캘 종속 작업이 이미 수행되었을 수 있습니다. 즉, 호출될 시점에는 다른 로캘로 안정적으로 전환하기에 too late입니다. 그렇게 하면 독립 실행형 Python 인터프리터 바이너리의 컨텍스트에서도 디코딩된 텍스트에 불일치가 발생하기 때문입니다.
따라서 Py_Initialize 가 호출되고 CPython이 구성된 로캘이 여전히 기본 C 로캘임을 감지하며 PYTHONCOERCECLOCALE=0이 설정되어 있지 않으면 다음 경고가 표시됩니다.:
Python runtime initialized with LC_CTYPE=C (a locale with default ASCII
encoding), which may cause Unicode compatibility problems. Using C.UTF-8,
C.utf8, or UTF-8 (if available) as alternative Unicode-compatible
locales is recommended.
(참고: 이 경고는 결국 기본적으로 표시되지 않도록 억제되었습니다. 자세한 내용은 위의 Implementation Note를 참조하십시오.)
이 경우 로캘 설정은 실제로 변경되지 않습니다.
대신 이 경고는 시스템 및 애플리케이션 통합 담당자에게 예상대로 제대로 작동하지 않을 것으로 예상되는 구성에서 Python 3을 실행하고 있음을 알립니다.
권장 사항을 제공하는 두 번째 문장은 향후 운영 체제에 따라 조건부로 컴파일될 수 있지만(예: *BSD 시스템에서는 LC_CTYPE=UTF-8를 권장), 초기 구현에서는 위에 표시된 일반적인 공통 메시지만 사용합니다.
새로운 빌드 시 구성 옵션
위의 두 동작은 모두 기본적으로 활성화되지만, 이러한 기본 설정을 재정의하려는 재배포자를 위해 관련 구성 옵션과 전처리기 정의도 새로 추가됩니다.
로케일 강제 변환 동작은 --with[out]-c-locale-coercion 플래그로 제어되며, 이 플래그는 PY_COERCE_C_LOCALE 전처리기 정의를 설정합니다.
로케일 경고 동작은 --with[out]-c-locale-warning 플래그로 제어되며, 이 플래그는 PY_WARN_ON_C_LOCALE 전처리기 정의를 설정합니다.
(참고: 이 컴파일 시 경고 옵션은 결국 런타임 PYTHONCOERCECLOCALE=warn 옵션으로 대체되었습니다. 자세한 내용은 위의 구현 참고 사항을 참조하십시오.)
autotools 기반 빌드 시스템을 사용하지 않는 플랫폼(즉, Windows)에서는 이러한 전처리기 변수가 항상 정의되지 않습니다.
표준 스트림의 기본 오류 처리 변경 사항
Python 3.5부터 CPython은 현재 로케일이 C이고 PYTHONIOENCODING 환경 변수나 Py_setStandardStreamEncoding API를 사용하여 특정 오류 처리가 설정되지 않았음을 감지하면 표준 스트림(sys.stdin, sys.stdout)에 surrogateescape를 기본적으로 사용합니다. 다른 로케일에서는 표준 스트림의 기본 오류 처리기가 strict입니다.
이러한 동작을 유지하면서 로케일 강제 변환과 로케일을 명시적으로 구성하는 경우 사이에 동작상의 불일치가 발생하지 않도록, 강제 변환 대상 로케일(C.UTF-8, C.utf8 및 UTF-8)을 표준 스트림의 기본 오류 처리기로 surrogateescape를 사용하는 로케일 목록에 추가합니다.
sys.stderr의 기본 오류 처리기에는 변경을 제안하지 않으며, 계속 backslashreplace로 유지합니다.
Android의 로케일 설정 변경 사항
이 PEP의 다른 변경 사항과는 별개로, Android 시스템의 CPython은 현재 setlocale(LC_ALL, "")를 호출하는 부분에서 setlocale(LC_ALL, "C.UTF-8")를 호출하고, 현재 setlocale(LC_CTYPE, "")를 호출하는 부분에서 setlocale(LC_CTYPE, "C.UTF-8")를 호출하도록 업데이트됩니다.
이 Android 전용 동작은 다음과 같은 Android 고유의 세부 사항으로 인해 도입됩니다.
- Android에서는
setlocale에""를 전달하는 것이"C"를 전달하는 것과 같습니다. C.UTF-8로케일은 항상 사용할 수 있습니다.
플랫폼 지원 변경 사항
다음을 명시하는 “Legacy C Locale” 섹션이 PEP 11에 새로 추가됩니다:
- CPython 3.7부터 *nix 플랫폼은 레거시
C로케일의 대안으로C.UTF-8(전체 로케일),C.utf8(전체 로케일) 또는UTF-8(LC_CTYPE전용 로케일) 중 하나 이상을 제공해야 합니다. 레거시C로케일에서만 발생하고 적절하게 구성된 비ASCII 로케일에서는 재현할 수 없는 모든 유니코드 관련 통합 문제는 “수정하지 않음”으로 종료합니다.
근거
C 로케일 처리 개선
C 로케일의 ASCII 기본 인코딩이 현대적인 네트워크 서비스 개발에 전적으로 잘못된 선택이라는 점은 한동안 분명했습니다. Rust와 Go 같은 최신 언어는 이러한 기본값을 완전히 배제하고, 대신 운영 체제 인터페이스의 텍스트 인코딩으로 UTF-8을 사용하도록 시스템을 구성하는 것을 배포 요구 사항으로 삼았습니다. 마찬가지로 Node.js는 기본적으로 UTF-8을 가정하고(V8 JavaScript 엔진에서 상속한 동작), 로케일 인식 작업에 시스템 로케일 설정을 사용하도록 지정하려면 사용자 지정 빌드 설정이 필요합니다. JVM과 .NET CLR은 모두 애플리케이션과 애플리케이션 런타임(즉, 호스트 운영 체제가 아닌 JVM/CLR) 사이에서 텍스트를 전달할 때 UTF-16-LE를 기본 인코딩으로 사용합니다.
CPython의 과제는 네트워크 서비스 개발에 사용될 뿐만 아니라 대규모 애플리케이션의 임베디드 스크립팅 언어와 데스크톱 애플리케이션 개발 언어로도 광범위하게 사용된다는 점입니다. 이러한 경우에는 현대적인 네트워크 서비스 개발에서 새롭게 형성되는 관례보다 동일한 프로세스를 공유하는 다른 로케일 인식 구성 요소 및 사용자의 데스크톱 로케일 설정과 일관성을 유지하는 것이 더 중요합니다.
이 PEP의 핵심 전제는 이러한 all의 사용 사례에서 기본 “C” 로케일이 암시하는 ASCII 가정이 잘못된 선택이며, 또한 다음 가정이 타당하다는 것입니다.
- 데스크톱 애플리케이션 사용 사례에서는 프로세스 로케일이 이미 적절하게 구성되어 있을 것이며, 그렇지 않다면 운영 체제 제공자 또는 애플리케이션 개발자에게 보고하고 해결해야 하는 운영 체제 또는 임베딩 애플리케이션 수준의 문제입니다.
- 네트워크 서비스 개발 사용 사례(특히 Linux 컨테이너 기반)에서는 프로세스 로케일이 전혀 구성되지 않을 수 있으며, 그렇다면 CPython이 현재처럼 레거시 C의 ASCII 기본 인코딩을 신뢰하기보다는 Rust, Go 및 Node.js처럼 구성 요소가 자체 기본 인코딩을 적용할 것으로 예상합니다.
표준 IO 스트림에서 “surrogateescape” 오류 처리를 기본값으로 사용합니다.
레거시 C 기본값과 ASCII를 선호하는 텍스트 인코딩으로 간주하는 가정에서 벗어나도록 로캘을 강제 변환함으로써, 이 PEP는 Python 3.5에서 도입된 표준 IO 스트림의 “surrogateescape” 오류 처리기의 암묵적 사용([15])도 비활성화하며, PEP 540에서 제안한 UTF-8 모드로 동작할 때 surrogateescape를 자동으로 사용하는 기능도 비활성화합니다.
해당 동작을 조정하기 위한 또 하나의 구성 옵션을 도입하는 대신, 이 PEP는 stdin과 stderr의 오류 처리에 대한 “surrogateescape” 기본값을 잠재적인 세 가지 강제 변환 대상 로캘에도 적용할 것을 제안합니다.
이 동작의 목적은 운영 체제가 제공하는 텍스트 값이 해당 텍스트가 UTF-8로 인코딩되었다고 잘못 가정하더라도 일반적으로 Python 3 애플리케이션을 통해 투명하게 전달될 수 있도록 하는 것입니다.
특히 GB 18030 [12]은 모든 유니코드 코드 포인트를 처리하는 중국 국가 텍스트 인코딩 표준으로, 공식적으로 ASCII 및 UTF-8과 모두 호환되지 않지만, 그럼에도 불구하고 surrogate 이스케이프된 데이터로 처리되는 것을 흔히 허용합니다. GB 18030이 호환되지 않는 방식으로 ASCII 바이트 값을 재사용하는 지점은 UTF-8에서는 유효하지 않을 가능성이 높으므로 이스케이프되어 관련 ASCII 코드 포인트를 기준으로 분할하거나 검색하는 문자열 처리 작업에서는 불투명하게 됩니다. 특정 ASCII 또는 유니코드 코드 포인트 값을 기준으로 분할하거나 검색하지 않는 작업은 거의 확실히 올바르게 동작합니다.
마찬가지로 Shift-JIS [13]와 ISO-2022-JP [14]는 일본에서 여전히 널리 사용되며 ASCII 및 UTF-8과 모두 호환되지 않지만, 특정 ASCII 또는 유니코드 코드 포인트 값을 기준으로 분할하거나 검색하지 않는 텍스트 처리 작업은 허용합니다.
예를 들어 UTF-8(en_AU.UTF-8의 기본 인코딩)로 인코딩된 파일 하나와 GB-18030(zh_CN.gb18030의 기본 인코딩)으로 인코딩된 파일 하나, 이렇게 두 파일을 생각해 보십시오.:
$ python3 -c 'open("utf8.txt", "wb").write("ℙƴ☂ℌøἤ\n".encode("utf-8"))'
$ python3 -c 'open("gb18030.txt", "wb").write("ℙƴ☂ℌøἤ\n".encode("gb18030"))'
디스크에서는 이 두 파일이 매우 다르다는 것을 확인할 수 있습니다.:
$ python3 -c 'print("UTF-8: ", open("utf8.txt", "rb").read().strip()); \
print("GB18030:", open("gb18030.txt", "rb").read().strip())'
UTF-8: b'\xe2\x84\x99\xc6\xb4\xe2\x98\x82\xe2\x84\x8c\xc3\xb8\xe1\xbc\xa4\n'
GB18030: b'\x816\xbd6\x810\x9d0\x817\xa29\x816\xbc4\x810\x8b3\x816\x8d6\n'
그럼에도 불구하고 출력하기 전에 디코딩하기만 하면 두 파일 모두 터미널에 올바르게 렌더링할 수 있습니다.:
$ python3 -c 'print("UTF-8: ", open("utf8.txt", "r", encoding="utf-8").read().strip()); \
print("GB18030:", open("gb18030.txt", "r", encoding="gb18030").read().strip())'
UTF-8: ℙƴ☂ℌøἤ
GB18030: ℙƴ☂ℌøἤ
반대로 cat 및 이와 유사한 C/C++ 유틸리티가 흔히 하듯이 원시 바이트를 그대로 전달하기만 하면 그렇지 않습니다.:
$ LANG=en_AU.UTF-8 cat utf8.txt gb18030.txt
ℙƴ☂ℌøἤ
�6�6�0�0�7�9�6�4�0�3�6�6
중국어 로캘을 명시적으로 설정하더라도 GB-18030으로 인코딩된 파일을 올바르게 렌더링하는 데에는 도움이 되지 않습니다.:
$ LANG=zh_CN.gb18030 cat utf8.txt gb18030.txt
ℙƴ☂ℌøἤ
�6�6�0�0�7�9�6�4�0�3�6�6
문제는 명목상의 로캘과 관계없이 터미널 인코딩 설정이 UTF-8로 유지된다는 점입니다. iconv 유틸리티를 사용하면 GB18030 터미널을 에뮬레이트할 수 있습니다.:
$ cat utf8.txt gb18030.txt | iconv -f GB18030 -t UTF-8
鈩櫰粹槀鈩屆羔激
ℙƴ☂ℌøἤ
이렇게 하면 문제가 반전되어 GB18030 파일은 올바르게 렌더링되지만, UTF-8 파일은 “Python”을 비ASCII 문자로 렌더링할 때 예상되는 결과가 아니라 서로 무관한 한자 문자로 변환됩니다.
에뮬레이트된 GB18030 터미널 인코딩을 사용하는 경우 Python에서 UTF-8을 가정하면 두 파일이 모두 잘못 표시됩니다.:
$ python3 -c 'print("UTF-8: ", open("utf8.txt", "r", encoding="utf-8").read().strip()); \
print("GB18030:", open("gb18030.txt", "r", encoding="gb18030").read().strip())' \
| iconv -f GB18030 -t UTF-8
UTF-8: 鈩櫰粹槀鈩屆羔激
GB18030: 鈩櫰粹槀鈩屆羔激
그러나 로캘을 올바르게 설정하면 에뮬레이트된 GB18030 터미널이 이제 두 파일을 원래 의도한 대로 모두 표시합니다.:
$ LANG=zh_CN.gb18030 \
python3 -c 'print("UTF-8: ", open("utf8.txt", "r", encoding="utf-8").read().strip()); \
print("GB18030:", open("gb18030.txt", "r", encoding="gb18030").read().strip())' \
| iconv -f GB18030 -t UTF-8
UTF-8: ℙƴ☂ℌøἤ
GB18030: ℙƴ☂ℌøἤ
surrogateescape를 기본 IO 인코딩으로 유지하는 근거는 C 로캘에서 다음과 같은 유용한 동작을 보존하기 때문입니다.:
$ cat gb18030.txt \
| LANG=C python3 -c "import sys; print(sys.stdin.read())" \
| iconv -f GB18030 -t UTF-8
ℙƴ☂ℌøἤ
UTF-8 기반 로캘을 명시적으로 구성했을 때 현재 발생하는 예외로 되돌아가지 않기 위한 것입니다.:
$ cat gb18030.txt \
| python3 -c "import sys; print(sys.stdin.read())" \
| iconv -f GB18030 -t UTF-8
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/usr/lib64/python3.5/codecs.py", line 321, in decode
(result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x81 in position 0: invalid start byte
추가적인 이점으로, 강제 변환 대상 로캘 중 하나를 사용하도록 명시적으로 구성된 환경은 현재 C 로캘에서 기본적으로 활성화된 인코딩 투명성 동작을 암묵적으로 얻게 됩니다.
UTF-8 로캘 강제 변환 중 PYTHONIOENCODING 설정을 피합니다.
인터프리터 초기화 중 표준 스트림의 기본 처리를 변경하는 대신, 이 PEP의 이전 버전에서는 PYTHONIOENCODING을 utf-8:surrogateescape로 설정할 것을 제안했습니다. 이는 상당한 호환성 문제를 일으키는 것으로 밝혀졌습니다. surrogateescape 처리기는 Python 3.1 이상에만 존재하므로, 해당 구성으로 하위 프로세스에서 Python 2.7 프로세스를 실행하면 혼란스러운 방식으로 문제가 발생할 가능성이 있었습니다.
현재 설계에서는 이전 Python 버전이 표준 스트림에서 기본 strict 오류 처리를 유지하는 반면, Python 3.7 이상은 이러한 로캘이 로캘 강제 변환을 통해 도달한 경우가 아니라 명시적으로 구성된 경우에도 더 관대한 surrogateescape 처리기를 일관되게 사용합니다.
레거시 C 로캘에서 ASCII 기반 텍스트 처리에 대한 공식 지원을 중단합니다.
이제 10년 넘게 레거시 C 로캘에서 엄격한 바이트/텍스트 분리를 안정적으로 작동시키려고 노력해 왔습니다. 우리가 이를 작동시키지 못했을 뿐만 아니라 다른 누구도 성공하지 못했습니다. 확인된 유일하게 실행 가능한 대안은 바이트를 텍스트로 성급하게 디코딩하지 않고 그대로 전달하는 것(C/C++, Python 2.x, Ruby 등)이거나, 명목상의 C/C++ 로캘 인코딩을 대체로 무시하고 UTF-8(PEP 540, Rust, Go, Node.js 등) 또는 UTF-16-LE(JVM, .NET CLR)를 사용한다고 가정하는 것입니다.
이 PEP는 실제로 필요한 개발자가 LC_ALL=C, PYTHONCOERCECLOCALE=0을 설정하거나 --without-c-locale-coercion을 설정하는 사용자 지정 빌드를 실행하여 레거시 C 로캘에서 Python 코드를 실행하도록 계속 선택할 수 있도록 보장하지만, 동시에 해당 구성에서 Python 3의 유니코드 처리가 완전히 안정적일 것이라고 don’t기대하지 않음을 명확히 하며, 권장되는 대안은 더 적절한 로캘 설정을 사용하는 것입니다(사용 가능한 경우 PEP 540의 UTF-8 모드와 함께 사용하는 것도 가능합니다).
독립 실행형으로 실행할 때만 암묵적인 로캘 강제 변환을 제공합니다.
이 PEP에서 제안하는 설계의 가장 큰 단점은 CPython 런타임을 독립 실행형 애플리케이션으로 실행할 때와 더 큰 시스템 내부의 임베디드 구성 요소로 실행할 때(예: Apache httpd 내부에서 실행되는 mod_wsgi)의 동작 사이에 잠재적인 차이가 발생한다는 점입니다.
Python 3.x 개발 과정에서 Python 인터프리터가 초기화되는 시점에 잘못된 로캘 설정 처리를 개선하려는 여러 시도가 있었습니다. 드러난 문제는 인터프리터 시작 과정에서 이 시점이 궁극적으로 너무 늦다는 것입니다. 명령줄 인자와 환경 변수의 내용 같은 데이터가 Py_Initialize가 호출되기 훨씬 전에 운영 체제에서 이미 검색되고 잘못된 ASCII 텍스트 인코딩 가정에 따라 처리되었을 수 있습니다.
이러한 불일치로 인해 발생한 문제는 ASCII가 운영 체제 인터페이스에 사용하기에 적합한 인코딩이라는 운영 체제의 주장을 믿어서 발생한 문제보다 진단하고 디버깅하기가 훨씬 더 어려웠습니다. 이는 기본 CPython 바이너리에서도 마찬가지였으며, CPython을 스크립팅 엔진으로 내장하는 더 큰 C/C++ 애플리케이션은 말할 것도 없습니다.
이 PEP에서 제안하는 접근 방식은 독립 실행 시 인터프리터 시작 시퀀스에서 가능한 한 이른 시점으로 로캘 강제 변환을 이동하여 이 문제를 처리합니다. 즉, CPython 인터프리터 CLI의 기능을 구현하는 Py_Main()라이브러리 함수를 호출하기도 전에 C 수준의 main()함수에서 직접 수행합니다.
그러면 Py_InitializeAPI는 C로캘의 사용을 감지할 때 stderr에 명시적인 경고를 출력하기만 하며, 임베딩 애플리케이션이 더 합리적인 설정을 지정하도록 맡깁니다.
그렇기는 하지만 이 PEP의 참조 구현은 대부분의 기능을 공유 라이브러리에 추가하고, CLI는 무조건 새로운 비공개 API 두 개를 호출하도록 업데이트됩니다.:
if (_Py_LegacyLocaleDetected()) {
_Py_CoerceLegacyLocale();
}
이러한 API는 임베딩 애플리케이션을 위한 다른 “사전 구성” API와 유사합니다. 즉, Py_Initialize보다 before에 호출되도록 설계되며, 그에 따라 인터프리터가 초기화되는 방식을 변경합니다.
이러한 API가 공개된다면(이 PEP의 일부로 공개되든 후속 RFE에서 공개되든), 다른 임베딩 애플리케이션이 CPython CLI에 제안된 것과 동일한 동작을 재현하는 일은 간단할 것입니다.
레거시 동작 복원 허용
CPython 명령줄 인터프리터는 CPython을 내장하는 다른 애플리케이션에서 발생하는 결함을 조사하는 데 자주 사용되며, 이러한 애플리케이션은 이 PEP가 구현된 이후에도 여전히 C 로캘을 사용하고 있을 수 있습니다.
로캘 강제 변환 동작을 위한 간단한 켜기/끄기 스위치를 제공하면 디버깅 목적으로 이러한 애플리케이션의 동작을 훨씬 쉽게 재현할 수 있으며, 이 변경 사항이 적용된 버전을 실행할 때에도 이전 3.x 런타임의 동작을 더 쉽게 재현할 수 있습니다.
C 로캘 감지를 위한 LC_CTYPE 조회
LC_CTYPE는 CPython이 운영 체제에서 전달받은 환경 변수, 명령줄 인자 및 기타 텍스트 값의 암시적 디코딩을 수행하는 데 의존하는 실제 로캘 범주입니다.
따라서 현재 로캘 구성이 유니코드 처리 문제를 일으킬 가능성이 있는지 판단하려 할 때 이를 특별히 확인하는 것이 타당합니다.
UTF-8 로캘 강제 변환을 위한 LC_CTYPE 명시적 설정
Python은 현재 프로세스에서 다른 C/C++ ABI 호환 구성 요소를 통합하고, 하위 프로세스에서 임의의 언어로 작성된 구성 요소를 통합하는 접착 언어로 자주 사용됩니다.
LC_CTYPE를 C.UTF-8로 설정하는 것은 UTF-8로캘이 정의되지 않은 시스템에서 LC_CTYPE=UTF-8과 같은 설정이 제공되어 문제가 발생한 경우를 처리하는 데 중요합니다(예를 들어 Mac OS X ssh 클라이언트가 로캘 설정을 전달하도록 구성되어 있고 사용자가 Linux 서버에 로그인하는 경우).
이는 로캘 강제 변환이 활성화될 때 UTF-8 기반 로캘로의 전환이 현재 프로세스와 현재 환경을 상속하는 모든 하위 프로세스에 일관되게 적용되도록 보장하기에 충분합니다.
UTF-8 로캘 강제 변환 시 LANG 설정 방지
이 PEP의 이전 버전에서는 LC_CTYPE을 설정하는 것에 더해 LANG 범주와 무관한 기본 로케일을 설정하도록 제안했습니다.
이후에는 LC_CTYPE만 설정해도 PEP가 해결하려는 모든 문제가 있는 시나리오를 처리하기에 충분하다는 이유로 이 내용이 제거되었습니다. 반면 LANG도 설정하면 LANG이 올바르게 설정된 경우가 망가질 수 있고, 로캘 문제는 오로지 잘못된 LC_CTYPE설정 때문일 수 있기 때문입니다([22]).
예를 들어 Python 애플리케이션이 자체적으로 날짜 형식을 지정하는 대신 하위 프로세스에서 Linux의 date유틸리티를 호출하는 경우를 생각해 보십시오.:
$ LANG=ja_JP.UTF-8 LC_CTYPE=C date
2017年 5月 23日 火曜日 17:31:03 JST
$ LANG=ja_JP.UTF-8 LC_CTYPE=C.UTF-8 date # Coercing only LC_CTYPE
2017年 5月 23日 火曜日 17:32:58 JST
$ LANG=C.UTF-8 LC_CTYPE=C.UTF-8 date # Coercing both of LC_CTYPE and LANG
Tue May 23 17:31:10 JST 2017
Python 프로세스에서 LC_CTYPE만 업데이트하면 하위 프로세스는 계속 예상대로 동작합니다. 그러나 LANG도 업데이트하면 사실상 LC_TIME설정을 재정의하고 잘못된 날짜 형식 규칙을 사용하게 됩니다.
UTF-8 로캘 강제 변환 시 LC_ALL 설정 방지
이 PEP의 이전 버전에서는 LC_CTYPE를 설정하는 것과 함께 LC_ALL로캘 재정의를 설정하도록 제안했습니다.
이 PEP가 다루려는 모든 시나리오를 LC_CTYPE와 LANG만 설정해도 충분히 처리할 수 있으며, 다음과 같은 경우에 문제를 일으키지 않는다는 사실이 확인된 후 이 내용이 변경되었습니다.:
$ LANG=C LC_MONETARY=ja_JP.utf8 ./python -c \
"from locale import setlocale, LC_ALL, currency; setlocale(LC_ALL, ''); print(currency(1e6))"
¥1000000
현재 환경에 LC_ALL이 설정된 경우 로캘 강제 변환 건너뛰기
이제 로캘 강제 변환은 LC_CTYPE와 LANG만 설정하므로 LC_ALL도 설정되어 있으면 아무런 효과가 없습니다. 이 경우 잘못된 로캘 강제 변환 알림이 출력되는 것을 방지하기 위해 강제 변환을 대신 완전히 건너뜁니다.
“UTF-8 모드”와 독립적으로 로캘 강제 변환 고려
이 PEP의 로캘 강제 변환과 PEP 540의 UTF-8 모드가 모두 Python 3.7에 도입될 가능성이 있는 상황에서는 두 변경 사항을 모두 적용하기보다 둘 중 하나만 적용하도록 제한할 수 있는지 질문해 보는 것이 타당합니다.
관련 PEP 540에서 제안한 UTF-8 모드에는 두 가지 주요 제한 사항이 있으므로, 이 PEP를 대체하기보다는 보완할 가능성이 있습니다.
우선, 이 PEP와 달리 PEP 540의 UTF-8 모드는 현재 전혀 구성할 수 없는 기본 동작을 변경할 수 있게 합니다. 바로 그 점이 이 제안을 흥미롭게 만드는 동시에, 완전히 검증되지 않은 접근 방식이 되게 합니다. 반대로, 이 PEP에서 제안하는 접근 방식은 이 PEP에서 논의하는 종류의 호환성 문제를 처리하기 위해 이미 수년간 사용되어 온 C 로케일 시스템(LC_CTYPE, LANG)과 Python 표준 스트림(PYTHONIOENCODING)의 기존 구성 설정을 직접 기반으로 합니다.
둘째로, 그러한 경험을 바탕으로 우리가 알고 있는 사실 중 하나는 제안된 로케일 강제가 CPython 자체뿐만 아니라 GNU readline처럼 표준 스트림과 상호 작용하는 확장 모듈의 문제도 해결할 수 있다는 점입니다. 예를 들어, PEP 538이 활성화된 CPython 빌드에서 다음과 같은 대화형 세션을 생각해 보십시오. 첫 번째 줄 이후의 각 줄은 “위쪽 화살표, 왼쪽 화살표 4번, 삭제, 엔터”를 수행하여 실행합니다.:
$ LANG=C ./python
Python 3.7.0a0 (heads/pep538-coerce-c-locale:188e780, May 7 2017, 00:21:13)
[GCC 6.3.1 20161221 (Red Hat 6.3.1-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print("ℙƴ☂ℌøἤ")
ℙƴ☂ℌøἤ
>>> print("ℙƴ☂ℌἤ")
ℙƴ☂ℌἤ
>>> print("ℙƴ☂ἤ")
ℙƴ☂ἤ
>>> print("ℙƴἤ")
ℙƴἤ
>>> print("ℙἤ")
ℙἤ
>>> print("ἤ")
ἤ
>>>
이는 제대로 작동하는 명령 기록 편집기에서 정확히 기대할 수 있는 동작입니다.
반대로, 로케일 설정을 갱신하지 않고 Python 수준의 스트림 인코딩 설정만 변경하면 이전 릴리스에서 현재 다음과 같은 일이 발생합니다.:
$ LANG=C PYTHONIOENCODING=utf-8:surrogateescape python3
Python 3.5.3 (default, Apr 24 2017, 13:32:13)
[GCC 6.3.1 20161221 (Red Hat 6.3.1-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print("ℙƴ☂ℌøἤ")
ℙƴ☂ℌøἤ
>>> print("ℙƴ☂ℌ�")
File "<stdin>", line 0
^
SyntaxError: 'utf-8' codec can't decode bytes in position 20-21:
invalid continuation byte
이러한 특정한 오작동은 CPython이 아니라 GNU readline에서 발생합니다. 명령 기록 편집이 UTF-8을 인식하지 못했기 때문에 기록 버퍼가 손상되었고, 표준 입력에 그런 엉터리 데이터가 전달되어 surrogateescape 오류 처리기조차 우회되었습니다. 또한 PEP 540의 UTF-8 모드를 기술적으로 업데이트하여 readline도 재구성할 수 있지만, 이는 CPython C API를 거치지 않고 표준 스트림과 상호작용할 가능성이 있는 하나의 확장 모듈일 뿐이며, CPython이 수행하는 모든 변경은 readline이 별도의 하위 프로세스가 아니라 Python 3.7의 일부로 직접 실행될 때에만 적용됩니다.
그러나 실제로 구성된 로케일을 변경하면 임베딩 애플리케이션을 변경하지 않아도 GNU readline이 제대로 작동하기 시작합니다.:
$ LANG=C.UTF-8 python3
Python 3.5.3 (default, Apr 24 2017, 13:32:13)
[GCC 6.3.1 20161221 (Red Hat 6.3.1-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print("ℙƴ☂ℌøἤ")
ℙƴ☂ℌøἤ
>>> print("ℙƴ☂ℌἤ")
ℙƴ☂ℌἤ
>>> print("ℙƴ☂ἤ")
ℙƴ☂ἤ
>>> print("ℙƴἤ")
ℙƴἤ
>>> print("ℙἤ")
ℙἤ
>>> print("ἤ")
ἤ
>>>
$ LC_CTYPE=C.UTF-8 python3
Python 3.5.3 (default, Apr 24 2017, 13:32:13)
[GCC 6.3.1 20161221 (Red Hat 6.3.1-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print("ℙƴ☂ℌøἤ")
ℙƴ☂ℌøἤ
>>> print("ℙƴ☂ℌἤ")
ℙƴ☂ℌἤ
>>> print("ℙƴ☂ἤ")
ℙƴ☂ἤ
>>> print("ℙƴἤ")
ℙƴἤ
>>> print("ℙἤ")
ℙἤ
>>> print("ἤ")
ἤ
>>>
Mac OS X, iOS 및 Android에서 C 로케일 강제와 경고 활성화
Mac OS X, iOS 및 Android에서 CPython은 이미 시스템 인터페이스에 UTF-8을 사용한다고 가정하며, 대부분의 다른 로케일 인식 구성 요소도 그렇게 할 것으로 예상합니다.
따라서 이 PEP는 해당 가정이 완전히 중복된다는 전제하에, 원래 이러한 플랫폼에서 빌드 시 로케일 강제와 경고를 비활성화하도록 제안했습니다.
그러나 후속 조사에 따르면 이러한 플랫폼에서 LANG=C을 명시적으로 구성할 경우 GNU readline과 같은 확장 모듈이 다른 *nix 시스템에서와 거의 동일한 방식으로 오작동하는 것으로 나타났으므로, 그 가정은 잘못된 것으로 밝혀졌습니다. [21]
또한 Mac OS X는 다른 *nix 환경(예: Linux 또는 Android)에 배포할 Python 소프트웨어의 개발 및 테스트 플랫폼으로도 자주 사용되며, Linux 역시 모바일 및 Mac OS X 애플리케이션의 개발 및 테스트 플랫폼으로 자주 사용됩니다.
따라서 이 PEP는 CPython의 autotools 기반 빌드 도구 체인을 사용하는 모든 플랫폼(즉, Windows 이외의 모든 곳)에서 로케일 강제 및 경고 기능을 기본적으로 활성화합니다.
구현
참조 구현은 GitHub의 CPython 저장소에 있는 Alyssa Coghlan의 포크에서 pep538-coerce-c-locale 기능 브랜치 [18]로 개발되고 있습니다. 작업 중인 PR은 [20]에서 확인할 수 있습니다.
이 참조 구현은 이슈 28180 [1]의 개선 요청뿐만 아니라 이슈 28997 [16]을 해결하는 데 필요한 Android 호환성 수정도 다룹니다.
이전 Python 3 릴리스로 백포팅
Python 3.6.x로 백포팅
이 PEP가 Python 3.7에 채택될 경우, 변경 사항을 특별히 최초 Python 3.6.x 릴리스로 백포팅하는 재배포자는 이를 수행할 수 있으며 그렇게 하도록 권장됩니다. 그러나 이러한 백포팅은 기본적으로 적절한 로케일을 제공하는 데 필요한 변경 사항과 함께 수행하거나, 그러한 로케일을 이미 일관되게 사용할 수 있는 플랫폼에 한해서만 수행해야 합니다.
적어도 Fedora 프로젝트는 곧 출시될 Fedora 26 릴리스에서 이 접근 방식을 추진할 계획입니다 [19].
다른 3.x 릴리스로 백포팅
제안된 동작 변경은 주로 ASCII 기반 기본 C 로케일에서 발생하는 Python 3의 현재 오작동을 해결하는 버그 수정으로 간주되지만, 여전히 CPython이 C 로케일 시스템과 상호 작용하는 방식에 상당히 중요한 변화를 나타냅니다. 따라서 일부 재배포자는 특정 사용자 기반의 필요와 관심에 따라 이를 더 이전의 Python 3.x 릴리스로 백포팅할 수 있지만, 이를 일반적인 관행으로 권장하지는 않습니다.
그러나 Python 3 환경(예: 기본 컨테이너 이미지)을 이러한 구성 설정을 기본값으로 사용하도록 구성하는 것은 허용되며 권장됩니다.
감사의 말
이 PEP에서 제안하는 로케일 강제 접근 방식은 Armin Ronacher가 click 명령줄 유틸리티 개발 프레임워크에서 이 문제를 처리한 방식에서 직접 영감을 받았습니다 [2]:
$ LANG=C python3 -c 'import click; cli = click.command()(lambda:None); cli()'
Traceback (most recent call last):
...
RuntimeError: Click will abort further execution because Python 3 was
configured to use ASCII as encoding for the environment. Either run this
under Python 2 or consult http://click.pocoo.org/python3/ for mitigation
steps.
This system supports the C.UTF-8 locale which is recommended.
You might be able to resolve your issue by exporting the
following environment variables:
export LC_ALL=C.UTF-8
export LANG=C.UTF-8
이 변경 사항은 원래 Fedora의 시스템 Python 3.6 패키지 [3]를 위한 다운스트림 패치로 제안되었으며, 이후 재배포자가 이전 버전으로 백포트할 수 있도록 허용하는 절을 포함한 Python 3.7용 PEP로 재구성되었습니다. 업스트림 패치 개발과 병행하여 Charalampos Stratakis는 Fedora 26 백포트 작업을 진행하면서 제안된 변경 사항의 실용적 실행 가능성에 대한 피드백을 제공하고 있습니다.
초기 초안은 토론을 위해 Python Linux SIG에 게시되었고 [10], 이후 해당 토론과 Victor Stinner의 PEP 540에서의 작업을 바탕으로 수정되었습니다 [11].
이 PEP 전체의 유니코드 처리 예제에 사용된 “ℙƴ☂ℌøἤ” 문자열은 Ned Batchelder의 훌륭한 “Pragmatic Unicode” 발표 자료에서 가져온 것입니다 [9].
Stephen Turnbull은 쓰쿠바대학교(筑波大学)에서 정기적으로 마주하는 텍스트 인코딩 처리 문제에 관해 오랫동안 귀중한 통찰을 제공해 왔습니다.
참조
Copyright
This document has been placed in the public domain under the terms of the CC0 1.0 license: https://creativecommons.org/publicdomain/zero/1.0/