PEP 756 – PyUnicode_Export() 및 PyUnicode_Import() C 함수 추가
- Author:
- Victor Stinner <vstinner at python.org>
- PEP-Delegate:
- C API Working Group
- Discussions-To:
- Discourse thread
- Status:
- Withdrawn
- Type:
- Standards Track
- Created:
- 13-Sep-2024
- Python-Version:
- 3.14
- Post-History:
- 14-Sep-2024
- Resolution:
- 29-Oct-2024
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
제한된 C API 버전 3.14에 다음 함수를 추가합니다:
PyUnicode_Export(): Python str 객체를Py_buffer뷰로 내보냅니다.PyUnicode_Import(): Python str 객체를 가져옵니다.
CPython에서 PyUnicode_Export()는 O(1) 복잡도를 가집니다. 메모리를 복사하지 않으며 변환도 수행하지 않습니다.
근거
PEP 393
PEP 393 “유연한 문자열 표현”은 Python 3.3에서 문자열 내부 구조가 세 가지 형식을 사용하도록 변경했습니다:
PyUnicode_1BYTE_KIND: 유니코드 범위 [U+0000; U+00ff], UCS-1, 문자당 1바이트.PyUnicode_2BYTE_KIND: 유니코드 범위 [U+0000; U+ffff], UCS-2, 문자당 2바이트.PyUnicode_4BYTE_KIND: 유니코드 범위 [U+0000; U+10ffff], UCS-4, 문자당 4바이트.
Python str 객체는 항상 가장 압축된 형식을 사용해야 합니다. 예를 들어, ASCII 문자만 포함하는 문자열은 UCS-1 형식을 사용해야 합니다.
PyUnicode_KIND() 함수를 사용하여 문자열이 사용하는 형식을 알 수 있습니다.
데이터에 접근하려면 다음 함수 중 하나를 사용할 수 있습니다:
PyUnicode_1BYTE_KIND에는PyUnicode_1BYTE_DATA()를 사용합니다.PyUnicode_2BYTE_KIND에는PyUnicode_2BYTE_DATA()를 사용합니다.PyUnicode_4BYTE_KIND에는PyUnicode_4BYTE_DATA()를 사용합니다.
최상의 성능을 얻으려면 C 확장은 이 세 가지 문자열 네이티브 형식 각각에 대해 하나씩, 세 개의 코드 경로를 가져야 합니다.
제한된 C API
PyUnicode_KIND()와 PyUnicode_1BYTE_DATA() 같은 PEP 393 함수는 제한된 C API에서 제외됩니다. UCS 형식에 특화된 코드를 작성할 수 없습니다. 제한된 C API를 사용하는 C 확장은 덜 효율적인 코드 경로와 문자열 형식만 사용할 수 있습니다.
예를 들어, MarkupSafe 프로젝트는 최상의 성능을 위해 UCS 형식에 특화된 C 확장을 가지고 있으므로 제한된 C API를 사용할 수 없습니다.
명세
API
제한된 C API 버전 3.14에 다음 API를 추가합니다:
int32_t PyUnicode_Export(
PyObject *unicode,
int32_t requested_formats,
Py_buffer *view);
PyObject* PyUnicode_Import(
const void *data,
Py_ssize_t nbytes,
int32_t format);
#define PyUnicode_FORMAT_UCS1 0x01 // Py_UCS1*
#define PyUnicode_FORMAT_UCS2 0x02 // Py_UCS2*
#define PyUnicode_FORMAT_UCS4 0x04 // Py_UCS4*
#define PyUnicode_FORMAT_UTF8 0x08 // char*
#define PyUnicode_FORMAT_ASCII 0x10 // char* (ASCII string)
타입 크기를 명확하게 정의하고 플랫폼이나 컴파일러에 의존하지 않도록 int 대신 int32_t 타입을 사용합니다. 더 자세한 근거는 Avoid C-specific Types를 참조하십시오.
PyUnicode_Export()
API:
int32_t PyUnicode_Export(
PyObject *unicode,
int32_t requested_formats,
Py_buffer *view)
유니코드 문자열 unicode의 내용을 requested_formats 중 하나의 형식으로 내보냅니다.
- 성공하면 view를 채우고 형식(
0보다 큰 값)을 반환합니다. - 오류가 발생하면 예외를 설정하고
-1을 반환합니다. view는 변경되지 않습니다.
PyUnicode_Export()를 성공적으로 호출한 후에는 view 버퍼를 PyBuffer_Release()로 해제해야 합니다. 버퍼의 내용은 해제될 때까지 유효합니다.
버퍼는 읽기 전용이므로 수정해서는 안 됩니다.
문자열 길이를 얻으려면 view->len 멤버를 사용해야 합니다. 버퍼는 후행 NUL 문자로 끝나야 하지만, 문자열 안에 NUL 문자가 포함될 수 있으므로 이에 의존하는 것은 권장하지 않습니다.
유니코드 문자열 unicode와 view는 NULL이어서는 안 됩니다.
사용 가능한 형식:
| 상수 식별자 | 값 | 설명 |
|---|---|---|
PyUnicode_FORMAT_UCS1 |
0x01 |
UCS-1 문자열 (Py_UCS1*) |
PyUnicode_FORMAT_UCS2 |
0x02 |
UCS-2 문자열 (Py_UCS2*) |
PyUnicode_FORMAT_UCS4 |
0x04 |
UCS-4 문자열 (Py_UCS4*) |
PyUnicode_FORMAT_UTF8 |
0x08 |
UTF-8 문자열 (char*) |
PyUnicode_FORMAT_ASCII |
0x10 |
ASCII 문자열 (Py_UCS1*) |
UCS-2와 UCS-4는 네이티브 바이트 순서를 사용합니다.
requested_formats는 단일 형식이거나 위 표에 있는 형식들의 비트 단위 조합일 수 있습니다. 성공하면 반환되는 형식은 요청된 형식 중 단 하나로 설정됩니다.
향후 Python 버전에서 형식이 추가될 수 있다는 점에 유의하십시오.
메모리는 복사되지 않으며 변환도 수행되지 않습니다.
내보내기 복잡도
CPython에서 내보내기의 복잡도는 O(1)입니다. 메모리는 복사되지 않으며 변환도 수행되지 않습니다.
CPython과 PyPy에서 최상의 성능을 얻으려면 다음 4가지 형식을 지원하는 것을 권장합니다:
(PyUnicode_FORMAT_UCS1 \
| PyUnicode_FORMAT_UCS2 \
| PyUnicode_FORMAT_UCS4 \
| PyUnicode_FORMAT_UTF8)
PyPy는 기본적으로 UTF-8을 사용하므로 PyUnicode_FORMAT_UTF8 형식을 권장합니다. PyPy str 객체는 메모리에서 이동될 수 있으므로 이 형식은 메모리 복사가 필요합니다(PyPy는 이동형 가비지 컬렉터를 사용합니다).
Py_buffer 형식과 항목 크기
Py_buffer는 내보내기 형식에 따라 다음과 같은 형식과 항목 크기를 사용합니다:
| 내보내기 형식 | 버퍼 형식 | 항목 크기 |
|---|---|---|
PyUnicode_FORMAT_UCS1 |
"B" |
1바이트 |
PyUnicode_FORMAT_UCS2 |
"=H" |
2바이트 |
PyUnicode_FORMAT_UCS4 |
"=I" |
4바이트 |
PyUnicode_FORMAT_UTF8 |
"B" |
1바이트 |
PyUnicode_FORMAT_ASCII |
"B" |
1바이트 |
PyUnicode_Import()
API:
PyObject* PyUnicode_Import(
const void *data,
Py_ssize_t nbytes,
int32_t format)
지원되는 형식의 버퍼로부터 유니코드 문자열 객체를 생성합니다.
- 성공하면 새 문자열 객체에 대한 참조를 반환합니다.
- 오류가 발생하면 예외를 설정하고
NULL을 반환합니다.
data는 NULL이 아니어야 합니다. nbytes는 양수이거나 0이어야 합니다.
사용 가능한 형식은 PyUnicode_Export()를 참조하십시오.
UTF-8 형식
CPython 3.14는 내부적으로 UTF-8 형식을 사용하지 않으며, 문자열을 UTF-8로 내보내는 것을 지원하지 않습니다. 대신 PyUnicode_AsUTF8AndSize() 함수를 사용할 수 있습니다.
PyUnicode_FORMAT_UTF8 형식은 문자열에 UTF-8을 네이티브로 사용할 수 있는 대체 구현과의 호환성을 위해 제공됩니다.
ASCII 형식
PyUnicode_FORMAT_ASCII 형식으로 내보내기를 요청하면, ASCII 문자열에는 PyUnicode_FORMAT_UCS1 내보내기 형식이 사용됩니다.
PyUnicode_FORMAT_ASCII 형식은 주로 PyUnicode_Import()에서 문자열이 ASCII 문자만 포함하는지 검증하는 데 유용합니다.
서로게이트 문자와 내장된 NUL 문자
서로게이트 문자는 허용되며, 가져오고 내보낼 수 있습니다.
내장된 NUL 문자는 허용되며, 가져오고 내보낼 수 있습니다.
구현
하위 호환성
새로운 C API 함수만 추가되므로 하위 호환성에는 영향이 없습니다.
PEP 393 C API 사용 현황
2024년 3월에 PyPI 상위 7,500개 프로젝트를 대상으로 코드를 검색한 결과, 일반 C API로 UCS 형식을 가져오고 내보내는 프로젝트가 많다는 사실이 확인되었습니다.
PyUnicode_FromKindAndData()
25개 프로젝트가 PyUnicode_FromKindAndData()를 호출합니다:
- Cython (3.0.9)
- Levenshtein (0.25.0)
- PyICU (2.12)
- PyICU-binary (2.7.4)
- PyQt5 (5.15.10)
- PyQt6 (6.6.1)
- aiocsv (1.3.1)
- asyncpg (0.29.0)
- biopython (1.83)
- catboost (1.2.3)
- cffi (1.16.0)
- mojimoji (0.0.13)
- mwparserfromhell (0.6.6)
- numba (0.59.0)
- numpy (1.26.4)
- orjson (3.9.15)
- pemja (0.4.1)
- pyahocorasick (2.0.0)
- pyjson5 (1.6.6)
- rapidfuzz (3.6.2)
- regex (2023.12.25)
- srsly (2.4.8)
- tokenizers (0.15.2)
- ujson (5.9.0)
- unicodedata2 (15.1.0)
PyUnicode_4BYTE_DATA()
21개 프로젝트가 PyUnicode_2BYTE_DATA() 및/또는 PyUnicode_4BYTE_DATA()를 호출합니다:
- Cython (3.0.9)
- MarkupSafe (2.1.5)
- Nuitka (2.1.2)
- PyICU (2.12)
- PyICU-binary (2.7.4)
- PyQt5_sip (12.13.0)
- PyQt6_sip (13.6.0)
- biopython (1.83)
- catboost (1.2.3)
- cement (3.0.10)
- cffi (1.16.0)
- duckdb (0.10.0)
- mypy (1.9.0)
- numpy (1.26.4)
- orjson (3.9.15)
- pemja (0.4.1)
- pyahocorasick (2.0.0)
- pyjson5 (1.6.6)
- pyobjc-core (10.2)
- sip (6.8.3)
- wxPython (4.2.1)
기각된 아이디어
내장된 NUL 문자를 거부하고 끝에 NUL 문자를 요구하기
C에서는 문자열 끝에 NUL 문자가 있으면 편리합니다. 예를 들어, for (; *str != 0; str++) 루프로 문자를 순회할 수 있고, strlen()으로 문자열 길이를 구할 수 있습니다.
문제는 Python str 객체가 NUL 문자를 포함할 수 있다는 점입니다. 예: "ab\0c". 문자열에 내장된 NUL 문자가 있으면, 문자열의 끝을 찾을 때 NUL 문자에 의존하는 코드는 문자열을 잘라 버립니다. 이는 버그로, 심지어 보안 취약점으로 이어질 수 있습니다. 이전 논의는 이슈 Change PyUnicode_AsUTF8() to return NULL on embedded null characters를 참고하십시오.
내장된 NUL 문자를 거부하려면 문자열을 스캔해야 하며, 이는 O(n) 복잡도를 가집니다.
서로게이트 문자 거부하기
서로게이트 문자는 유니코드 범위 [U+D800; U+DFFF]에 속하는 문자입니다. 이러한 문자는 UTF-8과 같은 UTF 코덱에서 허용되지 않습니다. Python str 객체는 짝이 없는 서로게이트 문자를 임의로 포함할 수 있습니다. 예: "\uDC80".
서로게이트 문자를 거부하면 그러한 문자가 포함된 문자열을 내보낼 수 없게 됩니다. PyUnicode_Export() 호출자는 문자열 내용을 제어하지 못하므로, 이는 뜻밖이고 성가실 수 있습니다.
서로게이트 문자를 허용하면 모든 문자열을 내보낼 수 있으므로 이 문제를 피할 수 있습니다. 예를 들어, UTF-8 코덱을 surrogatepass 오류 핸들러와 함께 사용하여 서로게이트 문자를 인코딩하고 디코딩할 수 있습니다.
요청 시 변환
요청 시 형식을 변환할 수 있다면 편리할 것입니다. 예를 들어, UCS-4로만 내보내기를 요청하면 UCS-1과 UCS-2를 UCS-4로 변환하는 것입니다.
문제는 대부분의 사용자가 내보내기에 메모리 복사도 변환도 필요하지 않을 것, 즉 O(1) 복잡도를 기대한다는 점입니다. 모든 연산이 O(1) 복잡도를 갖는 API가 더 낫습니다.
UTF-8로 내보내기
CPython 3.14에는 문자열을 UTF-8로 인코딩하기 위한 캐시가 있습니다. UTF-8로 내보내기를 허용하고 싶은 유혹이 생깁니다.
문제는 UTF-8 캐시가 서로게이트 문자를 지원하지 않는다는 점입니다. 내보내기는 내장된 NUL 문자와 서로게이트 문자를 포함하여 문자열 내용 전체를 제공해야 합니다. 서로게이트 문자를 내보내려면 surrogatepass 오류 핸들러를 사용하는 별도의 코드 경로가 필요하며, 내보내기 작업마다 임시 버퍼를 할당해야 합니다: O(n) 복잡도.
내보내기는 O(1) 복잡도를 가져야 하므로 CPython에서 UTF-8을 내보내자는 아이디어는 폐기되었습니다.
논의
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.