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, 튜플)에 모은 다음 이를 복사해야 하며, 이로 인해 두 번째 할당과 두 번째 순회 비용이 발생합니다.- 새로 생성된 frozenset이 다른 코드에 노출되기 전에 그 frozenset에
PySet_Add()를 호출하는, 문서화된 패턴입니다. 이 방식은 생성 후에 불변 타입의 객체를 변경하며, 구현이 frozenset을 내부적으로 가변 상태로 유지하도록 강제합니다.
frozendict
PEP 814는 frozendict 내장 타입을 추가했으며, 이 타입은 C에서 PyFrozenDict_New(iterable)로 생성할 수 있습니다. 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와 섞어서 쓸 수 있으므로, 여러 컬렉션으로부터 한 번에 frozenset을 만들 수 있습니다. 이는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()가 받아들이는 것이라면 무엇이든 받아들입니다.
frozenset에 대한 PySet_Add() 소프트 폐지
frozenset에 대해 PySet_Add()를 호출하는 것은 소프트 폐지됩니다(PEP 387). 문서에서는 대신 PyFrozenSetWriter를 권장하며, 경고는 발생하지 않고 제거 일정도 없습니다. set 객체에 대한 PySet_Add()는 계속 완전히 지원됩니다.
구현이 frozenset은 생성된 뒤 절대 바뀌지 않는다고 가정할 수 있게 해 주는 PySet_Add()의 frozenset 지원 제거는 향후 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만 추가됩니다. frozenset에 대한 PySet_Add()의 소프트 폐지는 문서에만 적용됩니다. 기존 확장 모듈은 아무것도 바꾸지 않아도 계속 컴파일되고 실행됩니다.
보안 영향
알려진 바 없습니다.
교육 방법
두 API 모두 예제 코드와 함께 C API 레퍼런스에 문서화될 예정입니다.
기각된 아이디어
frozenset에 대한 PySet_Add()의 강한 폐지 예고
DeprecationWarning을 발생시키면 문서화된 패턴을 사용하는 확장 모듈이 동작하지 않게 됩니다. 이 PEP는 약한 폐지 예고까지만 다루며, 제거는 향후 PEP에 맡깁니다.
부록: CPython 내 마이그레이션 대상
CPython 자체의 C 코드에는 이 PEP가 대체하는 세 가지 패턴이 모두 들어 있습니다. 이 부분들은 참조 구현의 일부로 마이그레이션됩니다.
패턴 1 — 새로 생성한 frozenset에 대한 PySet_Add()
Python/marshal.c(TYPE_FROZENSET): 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는 내부적으로 이미 한 번의 패스로 frozendict를 만듭니다. 이 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.