[CPython] Type & Metaclass (PyType_Type)


[!info] 본 글은 Python 3.12 이하 CPython 기준으로 작성되었습니다. (no-GIL 이전 버전)

[!warning]
이 글에서 ‘자료형’이라는 의미의 type과 Python 내장 객체인 type 클래스가 모두 등장하고 있으니 혼동하지 않도록 주의하시길 바랍니다. ‘자료형’이라는 의미일 때는 평문 ’type’ 혹은 ‘타입’으로, Python 내장 객체의 경우 코드 스타일링 type으로 표기합니다.

Overview

[!Tip] Python에서, 모든 것은 객체입니다.

Python에서 모든 것은 Object이며, 자료형도 예외는 아니다.
CPython 구현상으로 PyTypeObject 구조체 변수가 바로 Python상에서의 type이자, PyObject 구조체의 extension이다. 즉 Python의 모든 built-in type(int, str, list…)은 CPython상으로 모두 PyTypeObject 구조체 변수이다. 이 부분에서 C/C++ 계열과 Python의 중요한 차이점이 생기는데, 바로 Python에는 primitive type(원시 타입)이라는 개념이 없다는 것이다.

Primitive Type

In computer scienceprimitive data types are a set of basic data types from which all other data types are constructed. … The most common primitive types are those used and supported by computer hardware, such as integers of various sizes, floating-point numbers, and Boolean logical values. Operations on such types are usually quite efficient. Primitive data types which are native to the processor have a one-to-one correspondence with objects in the computer’s memory, and operations on these types are often the fastest possible in most cases. - Primitive data type

primitive type은 컴퓨터 하드웨어와 일대일 대응(one-to-one correspondence) 관계를 가지는, 가장 빠르게 연산할 수 있는 data type으로 정의된다.

C/C++ 계열의 built-in type에는 대표적으로 int, char가 있으며, 이 타입을 연산하는데 사용하는 operator(e.g +,-, /, *…)들은 CPU의 특정 명령어와 일대일로 대응되기 때문에 컴파일러가 별도의 함수 호출이나 변환 과정없이 바로 번역할 수 있다. 따라서 C/C++ 계열의 built-in type들은 곧 primitive type이다.

[!note] C/C++ 계열의 built-in type들은 모두 primitive type이다.

그렇지만 Python의 built-in type인 int, string, list 타입들의 연산은 모두 overloading된 것으로, 특정 CPU 명령어와 일대일 대응되지 않는다.
심지어 int 타입의 + operator조차 Python에서는 미리 정의된 내부 메서드를 호출하는 것으로 동작한다.

n = 393                  # n은 int 타입
print(n + 7)             # 출력 결과: 400
print(n.__add__(7))      # 출력 결과: 400  ('+' 연산은 사실상 __add__ 메서드 호출)

따라서 ‘Python에 built-in type은 있지만, primitive type은 없다‘는 결론을 내릴 수 있다.

[!abstract] Python에는 primitive type이 존재하지 않는다. 즉, built-in type의 모든 연산은 ‘언어 차원에서 메서드로써 미리 정의’되어있으며, 내부적으로 이 ‘미리 정의’된 메서드들을 호출하는 것으로 구현되어있다.

또한, 위 코드의 n.__add__처럼 built-in 타입의 연산에 있어 ‘메서드 호출’을 한다는 점에 주목하자. +, - 등의 operator는 그저 overloading되어 내부적으로 해당 메서드를 호출하고 있을 뿐이다.
이처럼 Python에는 built-in 타입들에 있어 ‘CPU 명령어로 일대일 대응되지 않는, 미리 정의된’ 연산이 가능하다. 이런 구조가 가능하려면 Python에서는 int 타입 또한 클래스여야한다는 생각이 들지 않는가?

Python의 모든 자료형은 클래스다

아래와 같은 간단한 Python 코드로 직접 자료형을 출력해볼 수 있다.

print(type(393))              # <class 'int'>
print(type("pengdori"))       # <class 'str'>
print(type(3.14))             # <class 'float'>
print(type(True))             # <class 'bool'>
print(type([1, 2, 3]))        # <class 'list'>
print(type((1, 2, 3)))        # <class 'tuple'>
print(type({'a': 1, 'b': 2})) # <class 'dict'>

출력 결과의 의미는 다음과 같다:

  • ‘393’은 int 클래스의 인스턴스이다
  • “pengdori"는 str 클래스의 인스턴스이다
  • ‘3.14’는 float 클래스의 인스턴스이다

즉 Python의 built-in 타입들은 모두 클래스이다. user-defined type 또한 오직 Class 생성을 통해서만 정의할 수 있기 때문에, 이를 통해 Python의 모든 type은 class라는 것을 알 수 있다.

‘자료형’의 자료형은?

print(type(int))           # <class 'type'>
print(type(str))           # <class 'type'>
print(type(float))         # <class 'type'>
print(type(bool))          # <class 'type'>
print(type(list))          # <class 'type'>
print(type(tuple))         # <class 'type'>
print(type(dict))          # <class 'type'>

출력 결과의 의미는 다음과 같다:

  • int 클래스는 type 클래스의 인스턴스이다
  • str 클래스는 type 클래스의 인스턴스이다
  • float 클래스는 type 클래스의 인스턴스이다
  • ….

헷갈리면 안되는 점은, 이 built-in 타입 클래스들은 ‘type 클래스의 자식 클래스’가 아니라 ‘type 클래스의 인스턴스’ 라는 것이다. 즉, type이라는 도면을 이용해 built-in 클래스를 각기 생성한 것이다.
클래스를 인스턴스로 생성할 수 있는 특별한 클래스인 type은 대체 무엇일까?

Metaclass: type

In object-oriented programming, a metaclass is a class whose instances are classes themselves. Unlike ordinary classes, which define the behaviors of objects, metaclasses specify the behaviors of classes and their instances. - Wikipedia

Metaclass(메타 클래스)는 ‘그 인스턴스가 클래스인 클래스’를 의미하며, 앞서 보았듯이 Python의 type이 이에 속한다. Python 공식 문서에서 type과 Metaclass에 대해 다음과 같이 언급하고 있다.

Python Built-in Functions – type() With one argument, return the type of an object. The return value is a type object and generally the same object as returned by object.__class__. … With three arguments, return a new type object. This is essentially a dynamic form of the class statement.

‘A dynamic form of the class statement’가 바로 metaclass임을 의미한다.

3.3.3.1. Metaclasses
By default, classes are constructed using type(). The class body is executed in a new namespace and the class name is bound locally to the result of type(name, bases, namespace). The class creation process can be customized by passing the metaclass keyword argument in the class definition line, or by inheriting from an existing class that included such an argument.

이에 따르면, type 클래스가 Python의 기본 metaclass이지만 metaclass 키워드 지정을 통해 다른 metaclass로 클래스를 생성하는 것이 가능하다.
구체적으로는, 클래스를 정의할 때 필수 속성/메서드를 강제하거나 정의된 클래스를 자동으로 집계하는 등 직접 커스텀 기능을 정의할 수 있게 된다. 이것이 바로 metaclass라는 개념이 존재하는 이유이자 효용이라 할 수 있다.

class Meta(type): # 새로운 metaclass 정의
    pass

class MyClass(metaclass=Meta): # metaclass를 명시적으로 지정하여 클래스 생성
	pass

Implementation

앞서 Python layer에서 type이 무엇이고 어떤 역할을 하는지 알게 되었으니, 내부 구현은 쉽게 예상할 수 있다. (이전 글에서 다룬 PyObject에 대해서 어느정도 알고있다고 가정한다.)

PyType_Type

metaclass type은 Cpython에서 PyType_Type으로 구현되어있다.

PyTypeObject PyType_Type
This is the type object for type objects; it is the same object as type in the Python layer.

// Metaclass `type`
PyTypeObject PyType_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "type",
    // ...
    .tp_new = type_new,  // 클래스를 동적으로 생성할 때 호출하는 메서드
    .tp_init = type_init,
    .tp_call = type_call,
    // ...
};

이쯤 됐으면 당연하게 느껴질 수도 있겠지만 한번 더 언급하자면 PyType_Type 또한 Object, 즉 PyTypeObject이면서 PyObject이다.
여기서 특이한 점은 PyVarObject_HEAD_INIT(&PyType_Type, 0)와 같이 자기 자신을 참조(self-referential)하는 ‘순환 참조’를 한다는 것이다. 왜 이렇게 구현되었을까?
이렇게 구현된 이유는 바로 “type 클래스의 타입은 type” 이라는, 즉 계층 구조의 최상위가 자기 자신에 대해 닫혀 있는(closure) 완결성 있는 설계를 실현하기 위함이다.

[!note] type 클래스의 타입은 type이다.
따라서 print(type(type))의 출력 결과도 <class 'type'>이다.

다시 돌아가서, type이 metaclass라는 관점에서 주목해야할 부분은 클래스를 동적으로 생성할 때 호출하는 type_new 메서드이다.

// Objects/typeobject.c
static PyObject *
type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds)
{
	// ...
    /* Parse arguments: (name, bases, dict) */
    PyObject *name, *bases, *orig_dict;
    if (!PyArg_ParseTuple(args, "UO!O!:type.__new__",
                          &name,
                          &PyTuple_Type, &bases,
                          &PyDict_Type, &orig_dict))
    {
        return NULL;
    }

    type_new_ctx ctx = {
        .metatype = metatype,
        .args = args,
        .kwds = kwds,
        .orig_dict = orig_dict,
        .name = name,
        .bases = bases,
        // ...
        };
    
    PyObject *type = NULL;
	// ...
    type = type_new_impl(&ctx);
    Py_DECREF(ctx.bases);
    return type;
}

첫 번째 parameter인 PyTypeObject *metatype이 타입의 생성에 관여하고 있는 것을 볼 수 있다.

Example: int (built-in)

대표격 예시로 built-in 타입인 int의 구현을 보도록 하자.

[!info] Python 3의 int는 가변 크기로, 내부적으로 PyVarObject로 구현된다.

// Structure for an instance of the 'int' type
typedef struct {
    PyObject_VAR_HEAD   
    digit *ob_digit;
} PyLongObject;
// Descriptor (type object) for the 'int' type (the 'int' class itself)
PyTypeObject PyLong_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "int",                         // tp_name
    sizeof(PyLongObject),          // tp_basicsize
    0,                             // tp_itemsize (가변 사이즈 타입도 0)
    (destructor)long_dealloc,      // tp_dealloc
    (printfunc)long_print,         // tp_print
    (getattrfunc)0,                // tp_getattr
    (setattrfunc)0,                // tp_setattr
    // ...
};

PyVarObject_HEAD_INIT(&PyType_Type, 0)는 내부적으로 다음과 같이 확장된다.

PyVarObject_HEAD_INIT(&PyType_Type, 0)
-> { PyObject_HEAD_INIT(&PyType_Type) 0 }
-> { 1, &PyType_Type, 0 }
// 순서대로 ob_refcnt, ob_type, ob_size

python layer에서 ‘int 인스턴스 객체(PyLongObject)‘의 *ob_typePyLong_Type을 가리키고, ‘int 클래스 객체(PyLong_Type)‘의 *ob_typePyType_Type을 가리킨다.
그래서 print(type(393))<class 'int'>가, print(type(int))<class 'type'>가 출력됐던 것이다.

[!note] Python2에서는 PyClassObject가 Old-style class로서 별도로 존재했었으나, Python3에서 PyTypeObject (New-style class)와 통합되었다.

Reference

comments