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인 PyFrozenSetWriter와 PyFrozenDictWriter를 추가합니다. 라이터는 항목을 내부에 모으며, *_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, 튜플)에 모은 다음 이를 복사해야 하며, 이로 인해 두 번째 할당과 두 번째 순회 비용이 발생합니다.
  2. 새로 생성된 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_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는 내부적으로 이미 한 번의 패스로 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);

참고 문헌

  • PEP 782 — PyBytesWriter C API 추가
  • PEP 814 — frozendict 내장 타입 추가