PEP 839 – PyFrozenSetWriter 및 PyFrozenDictWriter C API
- Author:
- Donghee Na <donghee.na at python.org>
- Status:
- Draft
- Type:
- Standards Track
- Created:
- 15-Jul-2026
- Python-Version:
- 3.16
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
PyBytesWriter (PEP 782)의 설계를 따라 두 개의 빌더(“라이터”) C API인 PyFrozenSetWriter 및 PyFrozenDictWriter를 추가합니다. 라이터는 항목을 내부적으로 수집하며, *_Finish()는 변경 불가능한 객체인 frozenset 또는 frozendict (PEP 814)를 변경 가능한 중간 객체를 전혀 노출하지 않고 한 번의 순회로 생성합니다.
또한 frozenset에 PySet_Add()를 호출하는 것은 PyFrozenSetWriter를 사용하도록 소프트 폐기합니다 (PEP 387).
동기
C API에는 중간 컨테이너를 사용하거나 생성 후 객체를 변경하지 않고 frozenset 또는 frozendict를 항목별로 빌드할 방법이 없습니다:
frozenset
현재 C에서 frozenset을 빌드하는 방법은 두 가지뿐입니다:
PyFrozenSet_New(iterable): 모든 항목이 이미 하나의 이터러블에 들어 있는 경우에는 잘 작동합니다. 항목이 C에서 한 번에 하나씩 생성되거나 둘 이상의 컬렉션에서 오는 경우, 호출자는 먼저 해당 항목을 중간 변경 가능 컨테이너(set, list, tuple)에 수집한 다음 복사해야 하며, 이로 인해 두 번째 할당과 두 번째 순회 비용이 발생합니다.- 새로 생성된 frozenset을 다른 코드에 노출하기 전에 해당 frozenset에
PySet_Add()를 호출하는 문서화된 패턴입니다. 이는 생성 후 변경 불가능한 타입의 객체를 변경하며, 구현이 frozenset을 내부적으로 변경 가능한 상태로 유지하도록 강제합니다.
frozendict
PEP 814는 PyFrozenDict_New(iterable)를 사용하여 C에서 생성할 수 있는 frozendict 내장 타입을 추가했습니다. PyFrozenSet_New()와 마찬가지로 항목을 한 번에 하나씩 생성하거나 둘 이상의 매핑을 병합하는 코드는 먼저 중간 딕셔너리를 빌드한 다음 복사해야 합니다.
CPython 자체는 이런 방식으로 frozendict를 빌드하지 않습니다. frozendict() 생성자가 비공개 딕셔너리 함수를 사용하여 새 객체를 직접 채운 후 노출합니다. 확장 모듈은 이 경로를 사용할 수 없습니다. 라이터 API는 이 경로를 공개합니다.
근거
해시 테이블 기반의 두 변경 불가능한 컨테이너에 PEP 782의 라이터 패턴을 적용하면 다음과 같은 이점이 있습니다:
- 한 번의 순회로 생성 — 중간 컨테이너가 없고 복사도 없습니다.
- 정확한 크기 조정 —
Finish()는 최종 항목 수를 알고 있으므로 크기 조정 없이 정확히 알맞은 크기의 테이블을 빌드할 수 있습니다. - 실질적인 불변성 보장 — 반환되는 객체는 변경 가능한 상태에서 접근 가능했던 적이 없으므로
Finish()는 해시를 계산하고 캐시할 수 있으며, 생성 시점에 GC 추적 여부를 결정할 수 있고, 구현은 생성 후 객체가 절대 변경되지 않는다고 신뢰할 수 있습니다. - frozenset에 ``PySet_Add()``를 호출하는 패턴을 대체하는 방법 — set C API에서 변경 불가능한 객체를 변경하는 마지막 문서화 API입니다.
사양
PyFrozenSetWriter
typedef struct PyFrozenSetWriter PyFrozenSetWriter;
PyAPI_FUNC(PyFrozenSetWriter *) PyFrozenSetWriter_Create(
Py_ssize_t size_hint);
PyAPI_FUNC(int) PyFrozenSetWriter_Add(
PyFrozenSetWriter *writer,
PyObject *item);
PyAPI_FUNC(int) PyFrozenSetWriter_Update(
PyFrozenSetWriter *writer,
PyObject *iterable);
PyAPI_FUNC(PyObject *) PyFrozenSetWriter_Finish(
PyFrozenSetWriter *writer);
PyAPI_FUNC(void) PyFrozenSetWriter_Discard(
PyFrozenSetWriter *writer);
PyFrozenSetWriter_Create(size_hint)- 라이터를 생성합니다. size_hint는 예상 항목 수입니다(
0도 허용됨). 이는 제한이 아니라 힌트입니다. 오류가 발생하면 예외를 설정하고NULL을 반환합니다. PyFrozenSetWriter_Add(writer, item)- 작성기에 item (해시 가능)을 추가합니다.
set.add와 같이 중복된 항목은 무시합니다. 작성기는 item에 대한 강한 참조를 보유합니다. 성공하면0을 반환하고, 오류가 발생하면 예외를 설정한 상태로-1을 반환합니다. 오류가 발생해도 작성기는 유효한 상태로 유지됩니다. PyFrozenSetWriter_Update(writer, iterable)- iterable의 모든 항목을 추가합니다.
Add와 동일하게 오류를 처리합니다.Update는 원하는 횟수만큼 호출할 수 있고Add와 함께 사용할 수도 있으므로, 여러 컬렉션에서 한 번의 순회로 프로즌셋을 만들 수 있습니다. 이는 중간에 변경 가능한 셋을 사용하지 않으면PyFrozenSet_New()로는 할 수 없는 작업입니다. PyFrozenSetWriter_Finish(writer)- 수집된 항목을 포함하는 새
frozenset을 반환하고 작성기를 소멸시킵니다.Finish는 항목을 다시 복사하지 않습니다. 실패하면 예외를 설정한 상태로NULL을 반환합니다.PyBytesWriter_Finish와 마찬가지로 모든 경우에 작성기를 소멸시킵니다. PyFrozenSetWriter_Discard(writer)- 객체를 생성하지 않고 작성기를 소멸시키며, 작성기가 보유한 모든 참조를 해제합니다.
Discard(NULL)은 아무 작업도 하지 않습니다.
PyFrozenDictWriter
typedef struct PyFrozenDictWriter PyFrozenDictWriter;
PyAPI_FUNC(PyFrozenDictWriter *) PyFrozenDictWriter_Create(
Py_ssize_t size_hint);
PyAPI_FUNC(int) PyFrozenDictWriter_SetItem(
PyFrozenDictWriter *writer,
PyObject *key,
PyObject *value);
PyAPI_FUNC(int) PyFrozenDictWriter_Update(
PyFrozenDictWriter *writer,
PyObject *mapping);
PyAPI_FUNC(PyObject *) PyFrozenDictWriter_Finish(
PyFrozenDictWriter *writer);
PyAPI_FUNC(void) PyFrozenDictWriter_Discard(
PyFrozenDictWriter *writer);
생성, 오류 처리, Finish및 Discard는 PyFrozenSetWriter와 동일하게 동작합니다. PyFrozenDictWriter_Finish()는 새 frozendict을 반환합니다. SetItem은 해시 가능한 키를 요구하며, frozendict와 같이 기존 키를 덮어쓰되 첫 삽입의 위치를 유지합니다. Update는 PyFrozenDict_New()가 허용하는 모든 것을 허용합니다.
프로즌셋에서 PySet_Add()의 소프트 사용 중단
frozenset에 PySet_Add()를 호출하는 것은 소프트 사용 중단(PEP 387)입니다. 문서에서는 대신 PyFrozenSetWriter를 사용할 것을 권장하지만, 경고는 발생하지 않으며 제거 일정도 정해져 있지 않습니다. set 객체에서 PySet_Add()를 사용하는 것은 계속 완전히 지원됩니다.
생성 후 프로즌셋이 절대 변경되지 않는다고 구현이 가정할 수 있도록 PySet_Add()에서 프로즌셋 지원을 제거하는 일은 향후 PEP에 맡깁니다.
공통 규칙
- 작성기는
PyObject가 아니며 Python 코드에 절대로 노출해서는 안 됩니다. PyBytesWriter와 마찬가지로 작성기를 여러 스레드에서 동시에 사용해서는 안 됩니다.Finish()또는Discard()후에 작성기를 사용하는 것은 정의되지 않은 동작입니다.- 성공한 모든
Create()는 정확히 하나의Finish()또는Discard()와 짝을 이루어야 합니다. - 처음에는
PyBytesWriter와 마찬가지로 두 API 모두 제한 API에서 제외됩니다.
예제
PyObject *
build_keywords(const char *const *names, Py_ssize_t n)
{
PyFrozenSetWriter *w = PyFrozenSetWriter_Create(n);
if (w == NULL) {
return NULL;
}
for (Py_ssize_t i = 0; i < n; i++) {
PyObject *s = PyUnicode_FromString(names[i]);
if (s == NULL || PyFrozenSetWriter_Add(w, s) < 0) {
Py_XDECREF(s);
PyFrozenSetWriter_Discard(w);
return NULL;
}
Py_DECREF(s);
}
return PyFrozenSetWriter_Finish(w);
}
하위 호환성
새로운 API만 추가됩니다. 프로즌셋에서 PySet_Add()의 소프트 사용 중단은 문서에만 한정됩니다. 기존 확장은 변경 없이 계속 컴파일되고 실행됩니다.
보안 관련 영향
알려진 사항이 없습니다.
교육 방법
두 API 모두 예제 코드와 함께 C API reference에 문서화됩니다.
거부된 아이디어
프로즌셋에서 PySet_Add()의 강제 사용 중단
DeprecationWarning을 발생시키면 문서화된 패턴을 사용하는 확장 기능이 중단됩니다. 이 PEP에서는 완화된 사용 중단만 다루며, 제거는 향후 PEP에 맡깁니다.
부록: CPython에서의 마이그레이션 후보
CPython 자체의 C 코드에는 이 PEP가 대체하는 세 가지 패턴이 모두 포함되어 있습니다. 이러한 위치는 참조 구현의 일부로 마이그레이션됩니다.
패턴 1 — 새로 생성된 프로즌셋에서 PySet_Add()사용
Python/marshal.c(TYPE_FROZENSET): 프로즌셋이 변경되는 동안 숨겨진 상태를 유지하려면 지연 참조 등록도 필요합니다.Modules/_hashopenssl.c(openssl_md_meth_names)Modules/_ssl.c(ssl_enum_certificates)Modules/_abc.c(__abstractmethods__)Modules/_asynciomodule.c(_asyncio_awaited_bygetter)
패턴 2 — 중간 컨테이너를 PyFrozenSet_New()로 복사
Python/initconfig.c(PyConfig_Names): 리스트를 통해Objects/codeobject.c,Python/compile.c,Python/flowgraph.c(상수 인터닝 및 폴딩): 튜플을 통해Modules/_pickle.c(load_frozenset): 리스트를 통해
패턴 3 — 변경 가능한 딕셔너리를 PyFrozenDict_New()로 복사
Python/marshal.c(TYPE_FROZENDICT): 딕셔너리를 채운 다음PyFrozenDict_New()로 전체 테이블을 복사합니다.
Objects/dictobject.c는 이미 내부적으로 단일 패스로 프로즌딕셔너리를 생성하며, 이 PEP는 지원되는 API를 통해 해당 생성 경로를 사용할 수 있게 합니다.
마이그레이션 예시 (Python/marshal.c, TYPE_FROZENDICT):
// Before: build a dict, then copy it into a frozendict
v = PyDict_New();
for (;;) {
... PyDict_SetItem(v, key, val) ...
}
Py_SETREF(v, PyFrozenDict_New(v));
// After: build the frozendict directly, one pass, exact size
PyFrozenDictWriter *w = PyFrozenDictWriter_Create(n);
for (;;) {
... PyFrozenDictWriter_SetItem(w, key, val) ...
}
v = PyFrozenDictWriter_Finish(w);
참고 자료
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.