[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 science, primitive 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 byobject.__class__
. … With three arguments, return a new type object. This is essentially a dynamic form of theclass
statement.
‘A dynamic form of the class
statement’가 바로 metaclass임을 의미한다.
3.3.3.1. Metaclasses
By default, classes are constructed usingtype()
. The class body is executed in a new namespace and the class name is bound locally to the result oftype(name, bases, namespace)
. The class creation process can be customized by passing themetaclass
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 astype
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_type
은 PyLong_Type
을 가리키고, ‘int 클래스 객체(PyLong_Type
)‘의 *ob_type
은 PyType_Type
을 가리킨다.
그래서 print(type(393))
은 <class 'int'>
가, print(type(int))
는 <class 'type'>
가 출력됐던 것이다.
[!note] Python2에서는
PyClassObject
가 Old-style class로서 별도로 존재했었으나, Python3에서PyTypeObject
(New-style class)와 통합되었다.