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형식을 사용합니다. 더 자세한 근거는 C 전용 형식 피하기를 참조하십시오.
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은 문자열에 ASCII 문자만 포함되어 있는지 검증하는 PyUnicode_Import()에 주로 유용합니다.
서로게이트 문자와 포함된 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.