Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

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

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

PyBytesWriter (PEP 782)의 설계를 따라 두 개의 빌더(“라이터”) C API인 PyFrozenSetWriterPyFrozenDictWriter를 추가합니다. 라이터는 항목을 내부적으로 수집하며, *_Finish()는 변경 불가능한 객체인 frozenset 또는 frozendict (PEP 814)를 변경 가능한 중간 객체를 전혀 노출하지 않고 한 번의 순회로 생성합니다.

또한 frozenset에 PySet_Add()를 호출하는 것은 PyFrozenSetWriter를 사용하도록 소프트 폐기합니다 (PEP 387).

동기

C API에는 중간 컨테이너를 사용하거나 생성 후 객체를 변경하지 않고 frozenset 또는 frozendict를 항목별로 빌드할 방법이 없습니다:

frozenset

현재 C에서 frozenset을 빌드하는 방법은 두 가지뿐입니다:

  1. PyFrozenSet_New(iterable): 모든 항목이 이미 하나의 이터러블에 들어 있는 경우에는 잘 작동합니다. 항목이 C에서 한 번에 하나씩 생성되거나 둘 이상의 컬렉션에서 오는 경우, 호출자는 먼저 해당 항목을 중간 변경 가능 컨테이너(set, list, tuple)에 수집한 다음 복사해야 하며, 이로 인해 두 번째 할당과 두 번째 순회 비용이 발생합니다.
  2. 새로 생성된 frozenset을 다른 코드에 노출하기 전에 해당 frozenset에 PySet_Add()를 호출하는 문서화된 패턴입니다. 이는 생성 후 변경 불가능한 타입의 객체를 변경하며, 구현이 frozenset을 내부적으로 변경 가능한 상태로 유지하도록 강제합니다.

frozendict

PEP 814PyFrozenDict_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);

생성, 오류 처리, FinishDiscardPyFrozenSetWriter와 동일하게 동작합니다. PyFrozenDictWriter_Finish()는 새 frozendict을 반환합니다. SetItem은 해시 가능한 키를 요구하며, frozendict와 같이 기존 키를 덮어쓰되 첫 삽입의 위치를 유지합니다. UpdatePyFrozenDict_New()가 허용하는 모든 것을 허용합니다.

프로즌셋에서 PySet_Add()의 소프트 사용 중단

frozensetPySet_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_by getter)

패턴 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);

참고 자료

  • PEP 782 — PyBytesWriter C API 추가
  • PEP 814 — 프로즌딕셔너리 내장 타입 추가