Skip to content

타입 시스템 부트스트랩

type, object, weakref처럼 파이썬 타입 시스템의 뿌리가 되는 타입들은 정상적인 방법(이미 존재하는 type을 이용해 새 타입을 만드는 방법)으로는 만들 수 없다. type은 자기 자신의 타입이어야 하고, object는 모든 것의 조상이면서 동시에 그 자신도 객체여야 하기 때문이다. [[RustPython]]이 이 문제를 init_type_hierarchy() 안에서 unsafe 코드로 메모리를 직접 조립해 푸는 걸 보고, CPython은 같은 문제를 어떻게 푸는지 찾아봤다.

CPython은 이 순환을 런타임에 풀지 않는다. PyTypeObject를 힙에 할당하는 대신, C의 정적(static) 전역 구조체로 소스 코드에 박아버린다. Objects/typeobject.c에 있는 PyType_TypePyBaseObject_Type의 정의가 그것이다.

Objects/typeobject.c
PyTypeObject PyType_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0) // 자기 자신을 타입으로 참조
"type", /* tp_name */
sizeof(PyHeapTypeObject), /* tp_basicsize */
...
0, /* tp_base — 아직 비어있음 */
0, /* tp_dict — 아직 비어있음 */
...
};
PyTypeObject PyBaseObject_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0) // object의 타입은 type
"object", /* tp_name */
sizeof(PyObject), /* tp_basicsize */
...
0, /* tp_base — NULL (object는 조상이 없음) */
...
};

PyVarObject_HEAD_INIT(type, size)ob_type 필드에 넣을 값을 받는 매크로이다. 여기서 핵심은, C의 정적 구조체 초기화는 “주소를 적어 넣는 것”이지 “그 대상이 다 만들어질 때까지 기다리는 것”이 아니라는 점이다. &PyType_Type은 컴파일/링크 타임에 해결되는 심볼 주소일 뿐이라, PyType_Type 자신이 아직 초기화되지 않았어도(심지어 자기 자신을 가리켜도) 아무 문제가 없다. RustPython이 겪는 “닭이 먼저냐 달걀이 먼저냐”는 애초에 런타임 문제가 아니라 이미 컴파일러가 대신 풀어준 문제가 된다.

그래서:

  • PyType_Typeob_type&PyType_Type (자기 자신)
  • PyBaseObject_Typeob_type&PyType_Type
  • tp_base, tp_dict, tp_bases, tp_mro 같은 “행동”에 관련된 필드들은 일단 0/NULL로 비워둔다

즉 “타입이 무엇의 타입인가”라는 문제는 컴파일 타임에 이미 끝나 있고, 런타임에 남는 일은 딕셔너리·MRO·상속 슬롯 같은 부수적인 것들을 채우는 것뿐이다.

런타임 단계: PyType_Ready / _PyStaticType_InitBuiltin

Section titled “런타임 단계: PyType_Ready / _PyStaticType_InitBuiltin”

인터프리터가 시작되면 Objects/object.c_PyTypes_InitTypes()static_types[] 배열을 순회하며 각 타입을 준비시킨다. 이 배열의 맨 앞 두 개가 바로 PyBaseObject_Type, PyType_Type이다.

Objects/object.c
static PyTypeObject* static_types[...] = {
// The two most important base types: must be initialized first and
// deallocated last.
&PyBaseObject_Type,
&PyType_Type,
// PyStaticMethod_Type and PyCFunction_Type are used by PyType_Ready()
// on other types and so must be initialized first.
&PyStaticMethod_Type,
&PyCFunction_Type,
// 이후 int, str, list, dict, weakref ... 수백 개가 순서대로 이어짐
...
};
PyStatus
_PyTypes_InitTypes(PyInterpreterState *interp)
{
for (size_t i = 0; i < Py_ARRAY_LENGTH(static_types); i++) {
PyTypeObject *type = static_types[i];
if (_PyStaticType_InitBuiltin(interp, type) < 0) {
return _PyStatus_ERR("Can't initialize builtin type");
}
if (type == &PyType_Type) {
assert(PyBaseObject_Type.tp_base == NULL);
assert(PyType_Type.tp_base == &PyBaseObject_Type);
}
}
...
}

_PyStaticType_InitBuiltininit_static_typetype_ready()로 이어지는데, 바로 이 type_ready()PyType_Ready() 공개 API의 실체다. 내부는 여러 단계로 쪼개져 있다.

// Objects/typeobject.c — type_ready()
type_ready_set_dict(type); // tp_dict 딕셔너리 생성
type_ready_set_base(type); // tp_base 채우기
type_ready_set_type(type); // ob_type 채우기 (필요할 때만)
type_ready_set_bases(type, ...); // tp_bases 튜플 구성
type_ready_mro(type, ...); // MRO 계산
type_ready_set_new(type, ...);
type_ready_fill_dict(type); // 상속받은 슬롯을 dict에 채움
...

여기서 두 함수가 부트스트랩과 직접 관련된다.

type_ready_set_base: tp_base가 비어 있으면 object로 채운다. object 자신만 예외이다.

static int
type_ready_set_base(PyTypeObject *type)
{
PyTypeObject *base = type->tp_base;
if (base == NULL && type != &PyBaseObject_Type) {
base = &PyBaseObject_Type;
type->tp_base = base;
}
/* Now the only way base can still be NULL is if type is &PyBaseObject_Type. */
...
}

type_ready_set_type: ob_type이 비어 있으면 채우는데, 주석이 상황을 정확히 설명한다.

static int
type_ready_set_type(PyTypeObject *type)
{
/* base != NULL 체크는 사실 불필요하다. base가 NULL인 경우는
type이 &PyBaseObject_Type일 때뿐이고, 우리는 이미 그 ob_type이
NULL이 아님을 안다 (컴파일 시점에 &PyType_Type으로 초기화되어 있으므로).
하지만 coverity는 그걸 모른다. */
PyTypeObject *base = type->tp_base;
if (Py_IS_TYPE(type, NULL) && base != NULL) {
Py_SET_TYPE(type, Py_TYPE(base));
}
return 0;
}

Py_IS_TYPE(type, NULL), 즉 “ob_type이 아직 비어 있는가”는 PyType_Type/PyBaseObject_Type에서는 애초에 항상 거짓이다. 이미 정적 초기화 시점에 &PyType_Type으로 채워져 있기 때문이다. 이 함수는 나중에 동적으로 생성되는 (헤더에서 ob_type을 안 채우고 넘어온) C 확장 타입들을 위한 안전망일 뿐, type/object 자신에게는 사실상 아무 일도 하지 않는다.

RustPython은 weakref_typetype, object와 함께 손으로 조립해야 했지만, CPython의 _PyWeakref_RefType은 그냥 평범한 정적 타입 중 하나일 뿐이다.

Objects/weakrefobject.c
PyTypeObject
_PyWeakref_RefType = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
.tp_name = "weakref.ReferenceType",
.tp_basicsize = sizeof(PyWeakReference),
.tp_dealloc = weakref_dealloc,
...
// tp_base 명시 없음 → type_ready_set_base가 자동으로 &PyBaseObject_Type을 채움
};

static_types[] 배열 안에서도 int, str, list, dict보다도 뒤, 배열 끝자락 근처에 다른 빌트인 타입들과 함께 나열되어 있다. typeobject가 이미 컴파일 타임에 완성되어 있으므로, weakref는 그 위에서 만들어지는 또 하나의 타입일 뿐 별도의 부트스트랩 단계가 필요 없다.


참고