확장 모듈 격리하기

확장 모듈 격리하기 (Isolating Extension Modules)

이 가이드는 C-API 확장의 유지보수자를 위해 쓰인 글이에요. Python 자체가 라이브러리로 쓰이는 애플리케이션에서 그 확장을 더 안전하게 쓰고 싶은 분들이요. '여러 인터프리터가 한 프로세스에 공존할 때 C 확장이 왜 문제가 되는지'부터, 모듈별 상태(per-module state)와 힙 타입(heap type)이 어떻게 해법이 되는지까지 짚어 볼게요.

출처: Python 공식 문서

누가 이 글을 읽어야 하나

이 가이드는 Python 자체가 라이브러리로 쓰이는 애플리케이션에서 그 확장을 더 안전하게 쓰고 싶어 하는 C-API 확장의 유지보수자를 위해 쓰였어요.

배경

인터프리터는 Python 코드가 실행되는 컨텍스트예요. 구성(예: 임포트 경로)과 런타임 상태(예: 임포트된 모듈 집합)를 담고 있죠.

Python은 한 프로세스에서 여러 인터프리터를 실행하는 것을 지원해요. 두 가지 경우를 생각해 볼 수 있어요. 사용자는 인터프리터를 다음과 같이 실행할 수 있어요.

  • 순차적으로, 여러 번의 Py_InitializeEx() / Py_FinalizeEx() 주기를 거치면서, 그리고
  • 병렬로, Py_NewInterpreter() / Py_EndInterpreter()로 '서브 인터프리터'를 관리하면서요.

두 경우(및 그 조합) 모두 Python을 라이브러리에 임베딩할 때 가장 유용해요. 라이브러리는 일반적으로 그 라이브러리를 사용하는 애플리케이션에 대해 가정을 하면 안 되는데, 여기에는 프로세스 전체의 '주 Python 인터프리터'가 있다고 가정하는 것도 포함돼요.

역사적으로 Python 확장 모듈은 이 사용 사례를 잘 처리하지 못했어요. 많은 확장 모듈(일부 표준 라이브러리 모듈조차)이 프로세스별 전역 상태를 사용하는데, C 정적 변수가 쓰기 아주 쉽기 때문이에요. 그래서 인터프리터에 특정돼야 할 데이터가 인터프리터들 사이에 공유되는 일이 생기죠. 확장 개발자가 조심하지 않으면, 같은 프로세스에서 둘 이상의 인터프리터에 모듈이 로드될 때 크래시로 이어지는 엣지 케이스가 생기기 아주 쉬워요.

안타깝게도 인터프리터별 상태는 얻기 어려워요. 확장 작성자들은 개발할 때 여러 인터프리터를 염두에 두지 않는 경향이 있고, 그 동작을 테스트하는 것도 현재 번거롭거든요.

모듈별 상태의 등장

인터프리터별 상태에 초점을 맞추는 대신, Python의 C API는 더 세분화된 모듈별 상태를 더 잘 지원하는 방향으로 진화하고 있어요. 이는 C 레벨 데이터가 모듈 객체에 붙어야 한다는 뜻이에요. 각 인터프리터가 자신만의 모듈 객체를 만들어 데이터를 분리하죠. 격리를 테스트하려면, 단일 확장에 해당하는 여러 모듈 객체를 한 인터프리터에 로드하는 것도 가능해요.

모듈별 상태는 수명과 자원 소유권을 생각하는 쉬운 방법을 제공해요. 확장 모듈은 모듈 객체가 생성될 때 초기화되고, 해제될 때 정리되죠. 이런 면에서 모듈은 다른 어떤 PyObject*와 똑같아요. 생각해야 할 — 또는 잊어먹어야 할 — '인터프리터 종료 훅'은 없어요.

서로 다른 종류의 '전역'에 대한 사용 사례가 있다는 점을 참고하세요. 프로세스별, 인터프리터별, 스레드별, 또는 태스크별 상태 같은 것들이요. 모듈별 상태를 기본값으로 하면 이것들도 여전히 가능하지만, 예외적인 경우로 취급해야 해요. 필요하다면 추가적인 주의와 테스트를 기울여야 하죠. (이 가이드는 그것들을 다루지 않아요.)

격리된 모듈 객체

확장 모듈을 개발할 때 명심해야 할 핵심은, 하나의 공유 라이브러리에서 여러 모듈 객체가 만들어질 수 있다는 거예요. 예를 들면:

>>> import sys
>>> import binascii
>>> old_binascii = binascii
>>> del sys.modules['binascii']
>>> import binascii  # create a new module object
>>> old_binascii == binascii
False

경험칙으로, 두 모듈은 완전히 독립적이어야 해요. 모듈에 특정된 모든 객체와 상태는 모듈 객체 안에 캡슐화돼야 하고, 다른 모듈 객체와 공유되지 않으며, 모듈 객체가 할당 해제될 때 정리돼야 하죠. 이건 어디까지나 경험칙이라 예외는 가능하지만(전역 상태 관리 참고), 엣지 케이스에 더 많은 생각과 주의가 필요할 거예요.

일부 모듈은 덜 엄격한 제약으로도 충분할 수 있지만, 격리된 모듈은 다양한 사용 사례에 걸쳐 동작하는 명확한 기대치와 지침을 세우기 더 쉽게 만들어 줘요.

놀라운 엣지 케이스

격리된 모듈은 몇 가지 놀라운 엣지 케이스를 만들긴 해요. 가장 주목할 점은, 각 모듈 객체가 보통 자신의 클래스와 예외를 다른 비슷한 모듈과 공유하지 않는다는 거예요. 위 예시에서 이어가면, old_binascii.Errorbinascii.Error가 별개의 객체라는 점을 주목하세요. 다음 코드에서 예외가 잡히지 않아요.

>>> old_binascii.Error == binascii.Error
False
>>> try:
...     old_binascii.unhexlify(b'qwertyuiop')
... except binascii.Error:
...     print('boo')
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
binascii.Error: Non-hexadecimal digit found

이건 예상된 거예요. 순수 Python 모듈도 똑같이 동작한다는 점을 주목하세요. 이건 Python이 동작하는 방식의 일부예요.

목표는 확장 모듈을 C 레벨에서 안전하게 만드는 것이지, 해킹이 직관적으로 동작하게 만드는 게 아니에요. sys.modules를 '수동으로' 변형하는 것은 해킹으로 간주돼요.

여러 인터프리터에서 모듈을 안전하게 만들기

전역 상태 관리하기

때로는 Python 모듈과 연관된 상태가 그 모듈에 특정된 게 아니라 프로세스 전체(또는 모듈보다 '더 전역인' 다른 무언가)에 특정될 수 있어요. 예를 들면:

  • readline 모듈은 터미널을 관리해요.
  • 회로 기판에서 돌아가는 모듈은 기판의 LED를 제어하고 싶어 해요.

이런 경우 Python 모듈은 전역 상태를 소유하기보다 그 상태에 대한 접근을 제공해야 해요. 가능하다면, 모듈의 여러 복사본이 (다른 라이브러리—Python용이든 다른 언어용이든—와 함께) 그 상태에 독립적으로 접근할 수 있게 모듈을 작성하세요. 그것이 불가능하다면 명시적 잠금을 고려해 보세요.

프로세스 전역 상태를 써야 한다면, 여러 인터프리터 문제를 피하는 가장 간단한 방법은 모듈이 프로세스당 한 번 이상 로드되는 것을 명시적으로 막는 거예요. '거부: 프로세스당 모듈 객체 하나로 제한하기'를 보세요.

모듈별 상태 관리하기

모듈별 상태를 쓰려면 다단계 확장 모듈 초기화를 사용하세요. 이것은 모듈이 여러 인터프리터를 올바르게 지원한다는 신호를 줘요.

PyModuleDef.m_size를 양수로 설정해서 모듈에 지역적인 그만큼의 바이트 저장소를 요청하세요. 보통은 모듈 특정 구조체의 크기로 설정되는데, 그 구조체가 모듈의 모든 C 레벨 상태를 저장할 수 있죠. 특히 C 코드가 동작하는 데 필요한 클래스(예외 포함, 정적 타입 제외)와 설정(예: csvfield_size_limit)에 대한 포인터를 여기에 두는 거예요.

참고

또 다른 옵션은 상태를 모듈의 __dict__에 저장하는 거예요. 하지만 사용자가 Python 코드에서 __dict__를 수정할 때 크래시하지 않도록 해야 해요. 이는 보통 C 레벨에서 오류·타입 검사를 해야 한다는 뜻이고, 그건 잘못 만들기 쉽고 충분히 테스트하기 어려워요.

하지만 모듈 상태가 C 코드에 필요하지 않다면, 그것을 __dict__에만 저장하는 게 좋은 생각이에요.

모듈 상태가 PyObject 포인터를 포함한다면, 모듈 객체는 그 객체들에 대한 참조를 보유하고 모듈 레벨 훅 m_traverse, m_clear, m_free를 구현해야 해요. 이들은 클래스의 tp_traverse, tp_clear, tp_free처럼 동작해요. 추가하면 작업이 좀 필요하고 코드가 길어져요. 깨끗하게 언로드될 수 있는 모듈의 대가인 셈이죠.

모듈별 상태를 가진 모듈의 예시가 xxlimited로 현재 제공돼 있고, 파일 맨 아래에 예시 모듈 초기화가 있어요.

거부: 프로세스당 모듈 객체 하나로 제한하기

음수가 아닌 PyModuleDef.m_size는 모듈이 여러 인터프리터를 올바르게 지원한다는 신호예요. 모듈이 아직 그렇게 되지 않았다면, 모듈을 프로세스당 한 번만 로드 가능하게 명시적으로 만들 수 있어요. 예를 들면:

// A process-wide flag
static int loaded = 0;

// Mutex to provide thread safety (only needed for free-threaded Python)
static PyMutex modinit_mutex = {0};

static int
exec_module(PyObject* module)
{
    PyMutex_Lock(&modinit_mutex);
    if (loaded) {
        PyMutex_Unlock(&modinit_mutex);
        PyErr_SetString(PyExc_ImportError,
                        "cannot load module more than once per process");
        return -1;
    }
    loaded = 1;
    PyMutex_Unlock(&modinit_mutex);
    // ... rest of initialization
}

모듈의 PyModuleDef.m_clear 함수가 향후 재초기화를 준비할 수 있다면, loaded 플래그를 지워야 해요. 이 경우 모듈은 여러 인스턴스가 동시에 존재하는 것을 지원하지 않지만, 예를 들어 Python 런타임 종료(Py_FinalizeEx()) 이후 재초기화(Py_Initialize()) 후 로드되는 것은 지원할 거예요.

함수에서 모듈 상태 접근하기

모듈 레벨 함수에서 상태에 접근하는 것은 간단해요. 함수는 첫 번째 인자로 모듈 객체를 받아요. 상태를 추출하려면 PyModule_GetState를 쓸 수 있어요.

static PyObject *
func(PyObject *module, PyObject *args)
{
    my_struct *state = (my_struct*)PyModule_GetState(module);
    if (state == NULL) {
        return NULL;
    }
    // ... rest of logic
}

참고

PyModule_GetState는 모듈 상태가 없을 때, 즉 PyModuleDef.m_size가 0일 때, 예외를 설정하지 않고 NULL을 반환할 수 있어요. 자신의 모듈에서는 m_size를 통제하므로 쉽게 방지할 수 있어요.

힙 타입 (Heap Types)

전통적으로 C 코드에 정의된 타입은 정적(static)이에요. 즉 코드에 직접 정의된 정적 PyTypeObject 구조체로, PyType_Ready()로 초기화되죠.

그런 타입은 필연적으로 프로세스 전체에 공유돼요. 모듈 객체들 사이에 공유하려면 그들이 소유하거나 접근하는 상태에 주의를 기울여야 해요. 가능한 문제를 제한하려고, 정적 타입은 Python 레벨에서 불변(immutable)이에요. 예를 들어 str.myattribute = 123을 할 수 없죠.

CPython 구현 세부 사항: 진정으로 불변인 객체를 인터프리터 사이에서 공유하는 것은, 그들이 가변 객체에 접근을 제공하지 않는 한 괜찮아요. 하지만 CPython에서 모든 Python 객체는 가변적인 구현 세부 사항 하나를 가져요. 바로 참조 카운트예요. 참조 카운트의 변경은 GIL에 의해 보호돼요. 그래서 인터프리터들 사이에 어떤 Python 객체를 공유하는 코드는 암묵적으로 CPython의 현재 프로세스 전체 GIL에 의존해요.

정적 타입은 불변이고 프로세스 전역이기 때문에 '자신의' 모듈 상태에 접근할 수 없어요. 그러한 타입의 어떤 메서드가 모듈 상태에 접근해야 한다면, 그 타입은 힙 할당 타입(줄여서 힙 타입)으로 변환돼야 해요. 이것들은 Python의 class 문으로 만든 클래스에 더 가깝게 대응돼요.

새 모듈에서는 기본적으로 힙 타입을 쓰는 게 좋은 경험칙이에요.

정적 타입을 힙 타입으로 바꾸기

정적 타입을 힙 타입으로 변환할 수 있지만, 힙 타입 API는 정적 타입에서 '무손실' 변환을 위해 설계되지 않았음을 주의하세요. 즉 주어진 정적 타입과 정확히 똑같이 동작하는 타입을 만드는 것을 의미해요. 그래서 새 API에서 클래스 정의를 다시 쓰다 보면 몇 가지 세부 사항(예: 피클 가능성이나 상속된 슬롯)을 의도치 않게 바꿀 가능성이 커요. 당신에게 중요한 세부 사항은 항상 테스트하세요.

다음 두 점을 특히 주의하세요(하지만 이게 전체 목록은 아닙니다):

  • 정적 타입과 달리, 힙 타입 객체는 기본적으로 가변이에요. Py_TPFLAGS_IMMUTABLETYPE 플래그를 써서 가변성을 막으세요.
  • 힙 타입은 기본적으로 tp_new를 상속하므로, Python 코드에서 인스턴스화할 수 있게 될 수 있어요. Py_TPFLAGS_DISALLOW_INSTANTIATION 플래그로 막을 수 있어요.

힙 타입 정의하기

힙 타입은 PyType_Spec 구조체(클래스의 설명 또는 '청사진')를 채우고 PyType_FromModuleAndSpec()을 호출해 새 클래스 객체를 구성해서 만들 수 있어요.

참고

PyType_FromSpec() 같은 다른 함수도 힙 타입을 만들 수 있지만, PyType_FromModuleAndSpec()은 모듈을 클래스와 연결해서 메서드에서 모듈 상태에 접근할 수 있게 해 줘요.

클래스는 일반적으로 모듈 상태(C에서 안전한 접근용)와 모듈의 __dict__(Python 코드에서 접근용)에 둘 다 저장해야 해요.

가비지 컬렉션 프로토콜

힙 타입의 인스턴스는 그 타입에 대한 참조를 보유해요. 이는 모든 인스턴스가 소멸되기 전에 타입이 파괴되지 않음을 보장하지만, 가비지 컬렉터가 깨뜨려야 하는 참조 순환을 초래할 수 있어요.

메모리 누수를 피하려면, 힙 타입의 인스턴스가 가비지 컬렉션 프로토콜을 구현해야 해요. 즉 힙 타입은 다음을 해야 해요.

  • Py_TPFLAGS_HAVE_GC 플래그를 가질 것.
  • Py_tp_traverse를 써서 타입을 방문하는 traverse 함수를 정의할 것(예: Py_VISIT(Py_TYPE(self))).

추가 고려 사항은 Py_TPFLAGS_HAVE_GCtp_traverse의 문서를 참고하세요.

힙 타입을 정의하는 API는 유기적으로 성장해서, 현재 상태에서는 쓰기가 다소 어색해요. 다음 절들이 흔한 문제를 안내할게요.

Python 3.8 이하에서의 tp_traverse

tp_traverse에서 타입을 방문해야 한다는 요구 사항은 Python 3.9에서 추가됐어요. Python 3.8 이하를 지원한다면, traverse 함수가 타입을 방문하면 안 되므로 더 복잡해져야 해요.

static int my_traverse(PyObject *self, visitproc visit, void *arg)
{
    if (Py_Version >= 0x03090000) {
        Py_VISIT(Py_TYPE(self));
    }
    return 0;
}

안타깝게도 Py_Version은 Python 3.11에서만 추가됐어요. 대신 다음을 사용하세요.

  • 안정적 ABI를 쓰지 않는다면 PY_VERSION_HEX, 또는
  • sys.version_info(PySys_GetObject()PyArg_ParseTuple()을 통해).

tp_traverse 위임하기

traverse 함수가 기본 클래스(또는 다른 타입)의 tp_traverse에 위임한다면, Py_TYPE(self)가 한 번만 방문되게 하세요. tp_traverse에서 타입을 방문할 것으로 기대되는 건 힙 타입뿐임을 주의하세요.

예를 들어 traverse 함수가 다음을 포함한다면:

base->tp_traverse(self, visit, arg)

…그리고 base가 정적 타입일 수 있다면, 다음도 포함해야 해요:

if (base->tp_flags & Py_TPFLAGS_HEAPTYPE) {
    // a heap type's tp_traverse already visited Py_TYPE(self)
} else {
    if (Py_Version >= 0x03090000) {
        Py_VISIT(Py_TYPE(self));
    }
}

tp_newtp_clear에서 타입의 참조 카운트를 처리할 필요는 없어요.

tp_dealloc 정의하기

타입이 커스텀 tp_dealloc 함수를 가진다면, 다음을 해야 해요.

  • 어떤 필드가 무효화되기 전에 PyObject_GC_UnTrack()을 호출할 것, 그리고
  • 타입의 참조 카운트를 감소시킬 것.

tp_free가 호출되는 동안 타입을 유효하게 유지하려면, 타입의 refcount를 인스턴스가 할당 해제된 뒤에 감소시켜야 해요. 예를 들면:

static void my_dealloc(PyObject *self)
{
    PyObject_GC_UnTrack(self);
    ...
    PyTypeObject *type = Py_TYPE(self);
    type->tp_free(self);
    Py_DECREF(type);
}

기본 tp_dealloc 함수가 이 일을 하므로, 타입이 tp_dealloc을 재정의하지 않는다면 추가할 필요가 없어요.

tp_free 재정의하지 않기

힙 타입의 tp_free 슬롯은 PyObject_GC_Del()로 설정해야 해요. 이게 기본값이에요. 재정의하지 마세요.

PyObject_New 피하기

GC로 추적되는 객체는 GC를 인지하는 함수로 할당해야 해요.

PyObject_New()PyObject_NewVar()를 쓴다면:

  • 가능하면 타입의 tp_alloc 슬롯을 얻어 호출하세요. 즉 TYPE *o = PyObject_New(TYPE, typeobj)를 다음으로 바꾸세요:
TYPE *o = typeobj->tp_alloc(typeobj, 0);
  • o = PyObject_NewVar(TYPE, typeobj, size)를 같은 것으로 바꾸되, 0 대신 size를 쓰세요.

위가 불가능하다면(예: 커스텀 tp_alloc 안에서), PyObject_GC_New()PyObject_GC_NewVar()를 호출하세요.

TYPE *o = PyObject_GC_New(TYPE, typeobj);

TYPE *o = PyObject_GC_NewVar(TYPE, typeobj, size);

클래스에서 모듈 상태 접근하기

PyType_FromModuleAndSpec()으로 정의된 타입 객체가 있다면, PyType_GetModule()을 호출해 연관 모듈을 얻고, 그다음 PyModule_GetState()로 모듈의 상태를 얻을 수 있어요.

지루한 오류 처리 보일러플레이트를 조금 아끼려면, 이 두 단계를 PyType_GetModuleState()로 결합할 수 있어요. 결과는 이러해요.

my_struct *state = (my_struct*)PyType_GetModuleState(type);
if (state == NULL) {
    return NULL;
}

일반 메서드에서 모듈 상태 접근하기

클래스의 메서드에서 모듈 레벨 상태에 접근하는 것은 조금 더 복잡하지만, Python 3.9에서 도입된 API 덕분에 가능해요. 상태를 얻으려면 먼저 정의하는 클래스를 얻고, 그다음 그 클래스에서 모듈 상태를 얻어야 해요.

가장 큰 걸림돌은 메서드가 정의된 클래스, 줄여서 그 메서드의 'defining class'를 얻는 거예요. defining class는 자신이 속한 모듈에 대한 참조를 가질 수 있어요.

defining class를 Py_TYPE(self)와 혼동하지 마세요. 메서드가 타입의 서브클래스에서 호출되면 Py_TYPE(self)는 그 서브클래스를 가리키는데, 그 서브클래스는 당신의 모듈과 다른 모듈에 정의돼 있을 수 있어요.

참고

다음 Python 코드가 개념을 설명할 수 있어요. Base.get_defining_classtype(self) == Sub여도 Base를 반환해요.

class Base:
    def get_type_of_self(self):
        return type(self)

    def get_defining_class(self):
        return __class__

class Sub(Base):
    pass

메서드가 'defining class'를 얻으려면 METH_METHOD | METH_FASTCALL | METH_KEYWORDS 호출 규약과 대응하는 PyCMethod 시그니처를 사용해야 해요.

PyObject *PyCMethod(
    PyObject *self,               // object the method was called on
    PyTypeObject *defining_class, // defining class
    PyObject *const *args,        // C array of arguments
    Py_ssize_t nargs,             // length of "args"
    PyObject *kwnames)            // NULL, or dict of keyword arguments

defining class를 얻었으면 PyType_GetModuleState()를 호출해 그 연관 모듈의 상태를 얻으세요.

예를 들면:

static PyObject *
example_method(PyObject *self,
        PyTypeObject *defining_class,
        PyObject *const *args,
        Py_ssize_t nargs,
        PyObject *kwnames)
{
    my_struct *state = (my_struct*)PyType_GetModuleState(defining_class);
    if (state == NULL) {
        return NULL;
    }
    ... // rest of logic
}

PyDoc_STRVAR(example_method_doc, "...");

static PyMethodDef my_methods[] = {
    {"example_method",
      (PyCFunction)(void(*)(void))example_method,
      METH_METHOD|METH_FASTCALL|METH_KEYWORDS,
      example_method_doc}
    {NULL},
}

슬롯 메서드, getter와 setter에서 모듈 상태 접근하기

참고

이 기능은 Python 3.11에서 새로 생겼어요.

슬롯 메서드 — __add__를 위한 nb_add나 초기화를 위한 tp_new 같은 특수 메서드의 빠른 C 등가물 — 은 PyCMethod와 달리 defining class를 넘길 수 없는 아주 단순한 API를 가져요. PyGetSetDef로 정의된 getter와 setter도 마찬가지예요.

이런 경우 모듈 상태에 접근하려면 PyType_GetModuleByDef() 함수를 쓰고 모듈 정의를 넘기세요. 모듈을 얻었으면 PyModule_GetState()를 호출해 상태를 얻으세요.

PyObject *module = PyType_GetModuleByDef(Py_TYPE(self), &module_def);
my_struct *state = (my_struct*)PyModule_GetState(module);
if (state == NULL) {
    return NULL;
}

PyType_GetModuleByDef()는 메서드 결정 순서(즉 모든 슈퍼클래스)를 검색해서 대응하는 모듈을 가진 첫 번째 슈퍼클래스를 찾는 방식으로 동작해요.

참고

아주 이국적인 경우(같은 정의에서 만들어진 여러 모듈에 걸친 상속 체인)에는 PyType_GetModuleByDef()가 진짜 defining class의 모듈을 반환하지 못할 수도 있어요. 하지만 항상 같은 정의를 가진 모듈을 반환해서 호환 가능한 C 메모리 레이아웃을 보장해요.

모듈 상태의 수명

모듈 객체가 가비지 컬렉트되면 그 모듈 상태가 해제돼요. 모듈 상태의 (일부에 대한) 각 포인터에 대해 모듈 객체에 대한 참조를 보유해야 해요.

보통은 문제가 되지 않아요. PyType_FromModuleAndSpec()으로 만든 타입과 그 인스턴스는 모듈에 대한 참조를 보유하니까요. 하지만 외부 라이브러리용 콜백 같은 다른 곳에서 모듈 상태를 참조할 때는 참조 카운팅에 주의해야 해요.

열린 문제

모듈별 상태와 힙 타입 주변에는 몇 가지 문제가 여전히 열려 있어요.

상황을 개선하는 논의는 discuss 포럼의 c-api 태그 아래에서 하는 게 가장 좋아요.

클래스별 스코프

현재(Python 3.11 기준) CPython 구현 세부 사항에 의존하지 않고는 개별 타입에 상태를 붙이는 것이 불가능해요(그 세부 사항은 미래에 바뀔 수 있는데, 어쩌면 아이러니하게도 클래스별 스코프를 위한 적절한 해법을 허용하려고 바뀔지도 몰라요).

힙 타입으로의 무손실 변환

힙 타입 API는 정적 타입에서 '무손실' 변환을 위해 설계되지 않았어요. 즉 주어진 정적 타입과 정확히 똑같이 동작하는 타입을 만드는 것을 의미해요.

더 알아보기 (Learn more)

  • 확장 초기화와 PyModuleDef 구조체의 전모는 C API 문서의 모듈 초기화를 보세요.
  • PyType_FromModuleAndSpec(), PyType_GetModule(), PyType_GetModuleState() 같은 힙 타입 API는 타입 객체 문서를 참고하세요.