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

Python 개선 제안 한국어 번역

PEP 697 – 불투명 타입 확장을 위한 제한된 C API

Author:
Petr Viktorin <encukou at gmail.com>
Discussions-To:
Discourse thread
Status:
Final
Type:
Standards Track
Created:
23-Aug-2022
Python-Version:
3.12
Post-History:
24-May-2022, 06-Oct-2022
Resolution:
Discourse message

Table of Contents

번역·라이선스 안내

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

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found at PyType_Spec.basicsize, PyObject_GetTypeData(), Py_TPFLAGS_ITEMS_AT_END, Py_RELATIVE_OFFSET, PyObject_GetItemData().

×

See PEP 1 for how to propose changes.

초록

특정 (서브)클래스에 특화된 데이터만 코드가 다루도록 허용하여 일부 타입을 불투명 데이터로 확장할 수 있도록 제한된 C API 지원을 추가합니다.

이 메커니즘은 PyHeapTypeObject와 함께 사용할 수 있어야 합니다.

이 PEP에서는 메모리 레이아웃이 서로 다르고 이러한 기능에 대한 수요가 부족하다고 여겨지므로 tuple 또는 int와 같은 동적으로 크기가 조정되지 않는 가변 크기 객체의 확장을 허용하는 방안을 제안하지 않습니다. 이 PEP에서는 향후 필요할 경우 동일한 메커니즘을 통해 이를 수행할 여지를 남겨 둡니다.

동기

이 PEP가 해결하려는 핵심 문제는 사용자 정의 타입, 즉 type의 서브클래스인 메타클래스에 C 수준 상태를 연결하는 것입니다.

이는 다른 타입 시스템(예: C++, Java, Rust)을 Python 클래스로 노출하는 “래퍼”에서 흔히 필요합니다. 이러한 래퍼는 일반적으로 “래핑된” 비-Python 클래스에 대한 정보를 Python 타입 객체에 연결해야 합니다.

언어 래퍼 또는 코드 생성기를 사용하여 안정 ABI 확장을 생성할 수 있도록, 제한된 API에서도 이를 수행할 수 있어야 합니다. (안정 ABI를 제공하는 이점은 PEP 652를 참조하십시오.)

type을 확장하는 것은 더 일반적인 문제의 한 사례입니다. 즉, 느슨한 결합을 유지하면서 클래스를 확장하는 것, 다시 말해 슈퍼클래스가 사용하는 메모리 레이아웃에 의존하지 않는 것입니다. (전문 용어가 많습니다. list를 확장하는 구체적인 예는 Rationale을 참조하십시오.)

근거

불투명 타입 확장

제한된 API에서는 대부분의 structs가 불투명합니다. 크기와 메모리 레이아웃이 노출되지 않으므로 CPython의 새 버전(또는 C API의 대체 구현)에서 변경할 수 있습니다.

이는 일반적인 서브클래싱 패턴, 즉 인스턴스에 사용되는 기저 타입의 struct를 인스턴스에 사용되는 파생 타입의 struct의 첫 번째 요소로 만드는 방식이 작동하지 않음을 의미합니다. 코드를 사용하여 설명하면, 튜토리얼의 예제는 다음 struct를 사용하여 PyListObject (list)를 확장합니다.

typedef struct {
    PyListObject list;
    int state;
} SubListObject;

PyListObject가 불투명하므로(기능과 최적화가 구현될 때 변경할 수 있도록 하기 위해) 이는 제한된 API에서 컴파일되지 않습니다.

대신 이 PEP에서는 서브클래스에 필요한 상태만 포함하는 struct를 사용할 것을 제안합니다. 즉 다음과 같습니다.

typedef struct {
    int state;
} SubListState;

// (or just `typedef int SubListState;` in this case)

이제 서브클래스는 슈퍼클래스의 메모리 레이아웃(및 크기)에서 완전히 분리될 수 있습니다.

이는 현재도 가능합니다. 이러한 struct를 사용하려면:

  • 클래스를 생성할 때 PyListObject->tp_basicsize + sizeof(SubListState)PyType_Spec.basicsize로 사용하십시오.
  • 데이터에 액세스할 때는 PyListObject->tp_basicsize를 인스턴스(PyObject*) 내 오프셋으로 사용하십시오.

그러나 여기에는 단점이 있습니다.

  • 기반 타입의 basicsize가 적절하게 정렬되지 않을 수 있으므로, 이를 완화하지 않으면 일부 아키텍처에서 문제가 발생합니다. (새 릴리스에서 정렬이 변경될 경우 이러한 문제는 특히 까다로울 수 있습니다.)
  • PyTypeObject.tp_basicsize는 제한된 API에 노출되지 않으므로, 제한된 API를 지원하는 확장에서는 PyObject_GetAttrString(obj, "__basicsize__")를 사용해야 합니다. 이는 번거롭고, 예외적인 경우에는 안전하지 않습니다(Python 속성을 재정의할 수 있습니다).
  • 가변 크기 객체는 처리되지 않습니다(Extending variable-size objects 아래 참조).

이를 쉽게 만들고(또한 최대 성능보다 느슨한 결합을 선택하는 프로젝트의 권장 방식으로 만들기 위해) 이 PEP에서는 다음을 수행하는 API를 제안합니다.

  1. 클래스 생성 중에 SubListStatePyListObject에 “추가”해야 한다고 지정하되, list에 대한 추가 세부 정보를 전달하지 않습니다. (인터프리터 자체가 기반 타입에서 tp_basicsize와 같은 필요한 모든 정보를 가져옵니다.)

    이는 음수 PyType_Spec.basicsize로 지정합니다: -sizeof(SubListState).

  2. 인스턴스와 서브클래스 PyTypeObject*가 주어지면, SubListState에 대한 포인터를 가져옵니다. 이를 위해 새 함수인 PyObject_GetTypeData가 추가됩니다.

물론 베이스 클래스는 PyListObject로 제한되지 않습니다. 인스턴스 struct가 불투명하거나 릴리스 간에 불안정하거나 전혀 노출되지 않은 모든 베이스 클래스를 확장하는 데 사용할 수 있으며, 여기에는 type (PyHeapTypeObject)과 서드파티 확장(예를 들어 NumPy 배열 [1])도 포함됩니다.

추가 상태가 필요하지 않은 경우에는 basicsize를 0으로 지정할 수 있습니다. 이 경우 베이스의 tp_basicsize가 상속됩니다. (현재도 작동하지만, 명시적인 문서와 테스트가 없습니다.)

새 클래스의 tp_basicsize는 계산된 전체 크기로 설정되므로, 클래스를 검사하는 코드는 이전과 동일하게 계속 작동합니다.

가변 크기 객체 확장

느슨한 결합을 유지하면서 variable-sized objects를 서브클래스화하려면 추가로 고려해야 할 사항이 있습니다. 가변 크기 데이터가 서브클래스 데이터와 충돌할 수 있기 때문입니다(위 예의 SubListState).

현재 CPython은 이러한 충돌을 방지할 방법을 제공하지 않습니다. 따라서 불투명 클래스를 확장하는 제안된 메커니즘(음수 base->tp_itemsize)은 기본적으로 실패합니다.

여기서 멈출 수도 있지만, 이 메커니즘을 도입하게 된 타입인 — PyHeapTypeObject — 이 가변 크기이므로 이를 안전하게 서브클래스화할 방법이 필요합니다. 먼저 약간의 배경을 살펴보겠습니다:

가변 크기 레이아웃

가변 크기 객체에는 주요 메모리 레이아웃이 두 가지 있습니다.

int이나 tuple과 같은 타입에서는 가변 데이터가 고정된 오프셋에 저장됩니다. 서브클래스에 추가 공간이 필요하면, 가변 크기 데이터 뒤에 추가해야 합니다:

PyTupleObject:
┌───────────────────┬───┬───┬╌╌╌╌┐
│ PyObject_VAR_HEAD │var. data   │
└───────────────────┴───┴───┴╌╌╌╌┘

tuple subclass:
┌───────────────────┬───┬───┬╌╌╌╌┬─────────────┐
│ PyObject_VAR_HEAD │var. data   │subclass data│
└───────────────────┴───┴───┴╌╌╌╌┴─────────────┘

반면 PyHeapTypeObject와 같은 다른 타입에서는 가변 크기 데이터가 항상 인스턴스의 메모리 영역 끝에 위치합니다.:

heap type:
┌───────────────────┬──────────────┬───┬───┬╌╌╌╌┐
│ PyObject_VAR_HEAD │Heap type data│var. data   │
└───────────────────┴──────────────┴───┴───┴╌╌╌╌┘

type subclass:
┌───────────────────┬──────────────┬─────────────┬───┬───┬╌╌╌╌┐
│ PyObject_VAR_HEAD │Heap type data│subclass data│var. data   │
└───────────────────┴──────────────┴─────────────┴───┴───┴╌╌╌╌┘

첫 번째 레이아웃에서는 항목 배열에 빠르게 액세스할 수 있습니다. 두 번째 레이아웃에서는 서브클래스가 가변 크기 배열을 무시할 수 있습니다(데이터에 액세스할 때 객체 시작 부분으로부터의 오프셋을 사용한다고 가정합니다).

이 PEP는 PyHeapTypeObject에 초점을 맞추므로, 두 번째 변형에 대해 서브클래스화를 허용하는 API를 제안합니다. 첫 번째 변형에 대한 지원은 나중에 API 호환 변경으로 추가할 수 있습니다(다만 이 PEP의 작성자는 그 노력이 들 만한 가치가 있을지 의문을 제기합니다).

PyHeapTypeObject와 유사한 레이아웃을 사용하는 클래스 확장

이 PEP는 Py_TPFLAGS_ITEMS_AT_END라는 타입 플래그를 제안하며, 이 플래그는 PyHeapTypeObject와 유사한 레이아웃임을 나타냅니다. 이는 두 가지 방법으로 설정할 수 있습니다:

  • 슈퍼클래스가 플래그를 설정하여, 서브클래스 작성자가 itemsize가 관련된다는 사실을 신경 쓰지 않도록 하거나,
  • 새 서브클래스가 플래그를 설정하여, 작성자가 슈퍼클래스가 적합하다는 것을 알고 있음을 나타낼 수 있습니다(다만 아직 플래그를 사용하도록 업데이트되지 않았을 수도 있습니다).

음수 basicsize를 사용하여 가변 크기 타입을 확장하려면 이 플래그가 필요합니다.

플래그를 사용하는 대신, 서브클래스 작성자가 베이스가 호환 가능한 레이아웃을 사용한다는 것을 알고 있도록 요구할 수도 있습니다(예를 들어 문서를 통해). 이 PEP의 이전 버전에서는 이를 위한 새 PyType_Slot을 제안했습니다. 그러나 이는 설명하기 어려운 것으로 드러났으며, 서브클래스를 베이스 레이아웃과 분리한다는 개념에도 어긋납니다.

새 플래그는 가변 크기 타입을 안전하게 확장하는 데 사용됩니다. spec->basicsize < 0이고 base->tp_itemsize > 0인 타입을 생성하려면 이 플래그가 필요합니다.

또한 이 PEP는 새로운 Py_TPFLAGS_ITEMS_AT_END 플래그를 사용하는 경우, 주어진 인스턴스의 가변 크기 데이터를 가져오는 도우미 함수를 제안합니다. 이를 통해 필요한 포인터 연산을 API 뒤에 숨기며, 이 API는 향후 다른 레이아웃(잠재적으로 VM이 관리하는 레이아웃 포함)에 맞게 조정될 수 있습니다.

전체적인 그림

모든 경우가 포함되었는지 확인하기 쉽도록, 여기 무섭게 보이는 전체적인 의사 결정 트리가 있습니다.

Note

개별 경우는 따로 설명하는 편이 더 쉽습니다(초안 문서는 reference implementation을 참조하십시오).

  • spec->basicsize > 0: 현 상태는 변경되지 않습니다. (베이스 클래스 레이아웃을 알고 있습니다.)
  • spec->basicsize == 0: (basicsize를 상속합니다.)
    • base->tp_itemsize == 0: 항목 크기를 spec->tp_itemsize로 설정합니다. (현 상태는 변경되지 않습니다.)
    • base->tp_itemsize > 0: (가변 크기 클래스를 확장합니다.)
      • spec->itemsize == 0: 항목 크기를 상속합니다. (현 상태는 변경되지 않습니다.)
      • spec->itemsize > 0: 항목 크기를 설정합니다. (안전하게 사용하기 어렵지만, 이것이 CPython의 현재 동작입니다.)
  • spec->basicsize < 0: (basicsize를 확장합니다.)
    • base->tp_itemsize == 0: (고정 크기 클래스를 확장합니다.)
      • spec->itemsize == 0: 항목 크기를 0으로 설정합니다.
      • spec->itemsize > 0: 실패합니다. (ob_size를 추가해야 하는데, 이는 단순 타입에 대해서만 가능하며 해당 단순 레이아웃을 알고 있어야 합니다.)
    • base->tp_itemsize > 0: (가변 크기 클래스를 확장합니다.)
      • spec->itemsize == 0: (itemsize를 상속합니다.)
        • Py_TPFLAGS_ITEMS_AT_END 사용됨: itemsize를 상속합니다.
        • Py_TPFLAGS_ITEMS_AT_END 사용되지 않음: 실패합니다. (충돌이 발생할 수 있습니다.)
      • spec->itemsize > 0: 실패합니다. (항목 크기를 변경하거나 확장하는 작업은 안전하게 수행할 수 없습니다.)

spec->itemsize < 0로 설정하는 것은 항상 오류입니다. 이 PEP는 tp->itemsize를 단지 상속하는 대신 확장할 어떤 메커니즘도 제안하지 않습니다.

상대적 멤버 오프셋

퍼즐의 또 다른 조각은 PyMemberDef.offset입니다. 서브클래스별 struct(SubListState 위의 구조체)를 사용하는 확장 모듈은 “절대적” 오프셋(PyObject 구조체를 기준으로 함)이 아니라 “상대적” 오프셋(이 struct를 기준으로 함)을 지정할 방법을 얻게 됩니다.

한 가지 방법은 새 API를 사용하여 클래스를 생성할 때 “상대적” 오프셋을 자동으로 가정하는 것입니다. 그러나 이러한 암묵적 가정은 지나치게 의외일 수 있습니다.

더 명확하게 하기 위해, 이 PEP는 “상대적” 오프셋을 위한 새로운 플래그를 제안합니다. 적어도 처음에는 이 플래그가 오용을 확인하는 검사(및 검토자를 위한 힌트)로만 사용됩니다. 새 API와 함께 사용되는 경우 이 플래그가 있어야 하며, 그 외의 경우에는 사용해서는 안 됩니다.

명세

아래 코드 블록에서는 함수 헤더만 사양의 일부입니다. 그 밖의 코드(크기 및 오프셋 계산)는 초기 CPython 구현의 세부 사항이며 변경될 수 있습니다.

상대적 basicsize

PyType_Specbasicsize 멤버는 0 또는 음수가 될 수 있습니다. 이 경우 그 절댓값은 새 클래스의 인스턴스가 기본 클래스의 basicsize에 더해 얼마나 많은 추가 저장 공간을 필요로 하는지를 지정합니다. 즉, 결과 클래스의 basicsize는 다음과 같습니다.

type->tp_basicsize = _align(base->tp_basicsize) + _align(-spec->basicsize);

여기서 _alignalignof(max_align_t)의 배수가 되도록 올림합니다.

spec->basicsize가 0이면 대신 basicsize를 직접 상속합니다. 즉, 정렬하지 않고 base->tp_basicsize로 설정합니다. (이 기능은 이미 작동하며, 명시적인 테스트와 문서를 추가할 예정입니다.)

인스턴스에서 서브클래스에 고유한 메모리 영역, 즉 서브클래스가 기본 클래스에 더해 예약하는 “추가 공간”은 새 함수 PyObject_GetTypeData를 통해 사용할 수 있습니다. CPython에서는 이 함수를 다음과 같이 정의합니다.

void *
PyObject_GetTypeData(PyObject *obj, PyTypeObject *cls) {
    return (char *)obj + _align(cls->tp_base->tp_basicsize);
}

이 메모리 영역의 크기를 가져오는 또 다른 함수가 추가됩니다.

Py_ssize_t
PyType_GetTypeDataSize(PyTypeObject *cls) {
    return cls->tp_basicsize - _align(cls->tp_base->tp_basicsize);
}

결과는 -basicsize로 요청한 값보다 클 수 있습니다. 그 전체를 사용해도 안전합니다(예: memset으로 사용).

새로운 *Get*함수에는 문서에서 지적할 중요한 주의 사항이 있습니다. 이 함수는 음수 PyType_Spec.basicsize를 사용해 생성된 클래스에만 사용할 수 있습니다. 그 밖의 클래스에서는 동작이 정의되지 않습니다. (이 점으로 인해 위 코드는 cls->tp_baseNULL이 아니라고 가정할 수 있습니다.)

itemsize 상속

spec->itemsize가 0이면 tp_itemsize를 기본 클래스에서 상속합니다. (이 기능은 이미 작동하며, 명시적인 테스트와 문서를 추가할 예정입니다.)

새로운 타입 플래그 Py_TPFLAGS_ITEMS_AT_END가 추가됩니다. 이 플래그는 tp_itemsize가 0이 아닌 타입에만 설정할 수 있습니다. 이 플래그는 인스턴스의 가변 크기 부분이 인스턴스 메모리의 끝에 저장된다는 것을 나타냅니다.

기본 메타클래스(PyType_Type)가 이 플래그를 설정합니다.

새로운 함수 PyObject_GetItemData가 추가되어, 새 플래그가 설정된 타입에서 가변 크기 콘텐츠를 위해 예약된 메모리에 액세스할 수 있게 됩니다. CPython에서는 다음과 같이 정의합니다.

void *
PyObject_GetItemData(PyObject *obj) {
    if (!PyType_HasFeature(Py_TYPE(obj), Py_TPFLAGS_ITEMS_AT_END) {
        <fail with TypeError>
    }
    return (char *)obj + Py_TYPE(obj)->tp_basicsize;
}

이 함수는 처음에는 Limited API에 추가되지 않습니다.

음수 spec->basicsize를 사용하여 양수 base->itemsize를 가진 클래스를 확장할 때는 기본 클래스 또는 spec->flagsPy_TPFLAGS_ITEMS_AT_END가 설정되어 있지 않으면 실패합니다. (Extending variable-size objects에 전체 설명이 있습니다.)

음수 spec->basicsize를 사용하여 양수 spec->itemsize를 가진 클래스를 확장하면 실패합니다.

상대적 멤버 오프셋

음수 PyType_Spec.basicsize를 사용하여 정의된 타입에서는 Py_tp_members를 통해 정의된 멤버의 오프셋이 전체 PyObject 구조체가 아니라 추가 서브클래스 데이터에 상대적이어야 합니다. 이는 PyMemberDef.flags의 새로운 플래그 Py_RELATIVE_OFFSET로 나타냅니다.

초기 구현에서는 새로운 플래그가 중복됩니다. 이 플래그는 오프셋 의미의 변경을 명확히 하고 실수를 방지하는 데만 사용됩니다. 음수 basicsize와 함께 Py_RELATIVE_OFFSET사용하지 않는 것은 오류이며, 다른 어떤 컨텍스트에서 이를 사용하는 것도 오류입니다(즉, PyDescr_NewMember, PyMember_GetOne, PyMember_SetOne에 대한 직접 또는 간접 호출입니다).

CPython은 형식을 초기화할 때 오프셋을 조정하고 Py_RELATIVE_OFFSET 플래그를 해제합니다. 이는 다음을 의미합니다.

  • 생성된 형식의 tp_members는 입력 정의의 Py_tp_members 슬롯과 일치하지 않으며,
  • tp_members를 읽는 코드는 플래그를 처리할 필요가 없습니다.

새로운 API 목록

다음과 같은 새로운 함수/값을 제안합니다.

다음 항목은 Limited API/Stable ABI에 추가됩니다.

  • void * PyObject_GetTypeData(PyObject *obj, PyTypeObject *cls)
  • Py_ssize_t PyType_GetTypeDataSize(PyTypeObject *cls)
  • PyTypeObject.tp_flagsPy_TPFLAGS_ITEMS_AT_END 플래그
  • PyMemberDef.flagsPy_RELATIVE_OFFSET 플래그

다음 항목은 공개 C API에만 추가됩니다.

  • void *PyObject_GetItemData(PyObject *obj)

하위 호환성

알려진 하위 호환성 문제는 없습니다.

가정

이 구현은 type->tp_base->tp_basicsize 오프셋과 type->tp_basicsize 오프셋 사이에 있는 인스턴스의 메모리가(가변 길이 형식은 제외하고) type에 “속한다”고 가정합니다. 이는 명시적으로 문서화되어 있지는 않지만, CPython은 버전 3.11까지 서브클래스에 __dict__를 추가할 때 이에 의존했으므로 안전할 것입니다.

보안 영향

알려진 바가 없습니다.

지지

pybind11의 작성자는 원래 이 문제의 해결을 요청했으며(this list의 2번 항목 참조), 구현을 검증해 왔습니다(has been verifying the implementation).

HPy 프로젝트의 Florian은 일반적으로 API가 좋아 보인다고 말했습니다. (성능 우려에 대한 가능한 해결책은 아래 를 참조하십시오.)

이 내용을 가르치는 방법

초기 구현에는 참조 문서와 What’s New 항목이 포함되며, 이는 대상 독자인 C 확장 라이브러리 작성자에게 충분할 것입니다 –

참조 구현

참조 구현은 encukou/cpython GitHub 저장소의 extend-opaque branch에 있습니다.

향후 개선 가능성

정렬 및 성능

인스턴스 구조체에 alignof(max_align_t)보다 작은 정렬이 필요한 경우, 제안된 구현은 일부 공간을 낭비할 수 있습니다. 또한 정렬을 처리하면 서브타입에 대해 base->tp_basicsize가 적절히 정렬되어 있다고 가정할 수 있을 때보다 계산이 느려집니다.

다시 말해, 제안된 구현은 안전성과 사용 편의성에 중점을 두며, 이를 위해 공간과 시간을 희생합니다. 이것이 문제가 되는 것으로 밝혀지면 API를 손상하지 않고 구현을 조정할 수 있습니다:

  • 타입별 버퍼에 대한 오프셋을 저장할 수 있으므로, PyObject_GetTypeData는 사실상 (char *)obj + cls->ht_typedataoffset로 변환되며, 클래스에 포인터를 하나 추가하는 대가로 속도가 향상될 수 있습니다.
  • 그런 다음 새로운 PyType_Slot에서 원하는 정렬을 지정하여 인스턴스에 필요한 공간을 줄일 수 있습니다.

가변 크기 타입을 위한 다른 레이아웃

Py_TPFLAGS_ITEMS_AT_END와 같은 플래그를 추가하여 Extending variable-size objects에 설명된 “튜플과 같은” 레이아웃을 나타낼 수 있으며, 이 PEP에서 제안하는 모든 메커니즘도 이를 지원하도록 조정할 수 있습니다. 다른 레이아웃도 추가할 수 있습니다. 그러나 실제로 얻는 이점은 매우 적을 것으로 보이므로, 이는 이론적인 가능성일 뿐입니다.

거부된 아이디어

음수인 spec->basicsize대신 새로운 PyType_Spec플래그를 추가할 수도 있었습니다. 이 변경 사항을 최신 상태로 알지 못한 채 이러한 내부 요소에 접근하는 기존 코드에 대해서도 그 효과는 동일합니다. 이 상황에서는 필드 값의 의미가 변경되기 때문입니다.

각주