파이썬 클래스 내부구조 (Python Class Internals)
클래스(class)는 데이터와 기능을 함께 번들링하는 수단을 제공합니다. 새로운 클래스는 새로운 객체의 type을 생성하며, 해당 type의 새로운 인스턴스(instances) 생성을 가능하게 합니다. 각 클래스의 인스턴스는 그 상태를 유지하기 위해 속성들을 가질 수 있습니다. 또한, 클래스 인스턴스들은 그것의 상태를 변경하기 위해 methods들을 가질 수 있습니다 [1].
class A:
pass
print(type(A))
# 출력
<class 'type'>
이 글에서는 클래스(User-Defined class)에 대해 다음과 같은 사항들을 기술합니다:
- class 구조 - type과 metaclass
- MRO (Method Resolution Order)
- class를 구성하는 것들
- class / instance 생성
관련글:
class 구조 - type과 metaclass
위 코드의 print(type(A))를 실행하면 <class 'type'>이라는 결과가 발생합니다. 왜 'A'가 아닌 'type'이라는 것이 출력되는 걸까요?
cpython 살펴보기
파이썬에서 실행에 사용되는 값(interpreter loop가 evaluation stack에 있는 values 사용 시), PyObject로 이루어져 있습니다.
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;
} PyObject;
그러한 PyObject에는 PyTypeObject라는 필드가 존재하는데, 그 대략적인 코드는 아래와 같습니다:
struct _typeobject {
PyObject_VAR_HEAD
const char *tp_name; /* For printing, in format "<module>.<name>" */
Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */
/* Methods to implement standard operations */
destructor tp_dealloc;
Py_ssize_t tp_vectorcall_offset;
getattrfunc tp_getattr;
setattrfunc tp_setattr;
PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
or tp_reserved (Python 3) */
reprfunc tp_repr;
...
};
각 객체의 *ob_type에는 PyTypeObject를 각 객체의 특성에 따라 필드를 구성한 PyType_Type, PyBaseObject_Type, PyTupe_Type 등으로 채워지며, 객체의 special methods / attrs(파이썬 코드 상의 __name__, __hash__ 등)을 매핑합니다 (slots).
그러한 Type들은 이름(아래 NAME)을 가지고 있습니다:
#define INIT_TYPE(TYPE, NAME) \
do { \
if (PyType_Ready(TYPE) < 0) { \
return _PyStatus_ERR("Can't initialize " NAME " type"); \
} \
} while (0)
INIT_TYPE(&PyBaseObject_Type, "object");
INIT_TYPE(&PyType_Type, "type");
...
INIT_TYPE(&PyTuple_Type, "tuple")
...
return _PyStatus_OK();
#undef INIT_TYPE
}
print(type(A)) 실행 시, 'type'이 출력되는 이유도 PyType_Type과 class A가 모종의 관계를 가지고 있기 때문입니다.
아래 이미지와 같이, 파이썬 builtin type은 type의 직접적인 instance이고, User defined type은 type의 (c 언어)instance인 object의 (파이썬)instance인 것을 알 수 있습니다.
그렇기에 (하나의 예시인) builtin type인 tuple의 type을 살펴보면 'type'이 출력되는 것을 알 수 있습니다:
print(type(tuple))
# 출력
<class 'type'>
위 이미지에도 나와있듯이 user defined type은 PyBaseObject_Type를 root로 두는데, 이것은 몇 가지 디폴트 값을 넣어주기 위함입니다. 이 부분은 A 클래스의 MRO를 확인하여 알 수 있습니다.
for e in A.mro():
print(e.__name__)
#
A
object
Python 코드로 살펴보기
클래스는 object입니다. 그렇기에 다른 모든 object와 같이 어떤 것의 다른 것의 instance입니다. 모든 클래스는 디폴트로 위의 type에 해당되는 metaclass의 instance입니다 [8].
class A:
pass
a = A()
print(type(A))
# <class 'type'>
print(type(a))
# <class '__main__.A'>
print(isinstance(a, A))
# True
print(isinstance(A, type))
# True
간단하게 아래 이미지와 같이 metaclass, class, instance의 관계를 나타낼 수 있습니다:
그렇기에 type을 직접 상속해서 metaclass를 만들어 사용할 수 있습니다.
class Meta(type):
pass
class Complex(metaclass=Meta):
pass
print(type(Complex))
# <class '__main__.Meta'>
MRO (Method Resolution Order)
파이썬 클래스는 다중상속이 가능합니다. 그렇기 때문에 만약 여러 super 클래스에 동일한 이름을 가진 함수가 있을 때, 해당 함수를 호출했을 때 어떤 super 클래스에 속한 함수를 사용할지 선택하는 알고리즘이 필요합니다.
파이썬(3.9)은 버젼 2.3에서 적용된 C3 superclasss linearization을 사용하고 있습니다.
MRO는 super 클래스를 가지는 클래스에서 함수를 사용할 때 주로 고려하게 됩니다. 파이썬 사용 시에는, 자주 이런 MRO가 적용되는데 그 이유는 다음과 같습니다:
- 클래스 구조에서 살펴보았듯이, instance, class, metaclass는 상속 관계를 가집니다.
- 클래스 또는 인스턴스에서 속성에 대한 접근, 클래스 생성 등은 모두 위에서 살펴본 slots과 관련이 깊은 Magic methods를 통해 이뤄집니다.
예로, 아래와 같이 클래스 C의 MRO는 C -> A -> B인 것을 알 수 있습니다:
아래의 클래스 D에서는 MRO가 D -> C -> A -> B이기에 process()를 실행하면, C의 process()가 실행되는 것을 확인할 수 있습니다:
마지막 fail 케이스인 아래 이미지에서 C의 MRO 계산을 생각해보면,
C -> A -> B -> A였다가, 2번째 A는 후행하는 리스트에 존재하여 good candidate이 아니게 됩니다.
2번째 A를 제외한, C -> B -> A를 시도하면 C -> A -> B 인지 C -> B -> A인지 우선순위를 알 수 없게 됩니다. 그렇기에 아래와 같은 TypeError를 일으킵니다. 이렇게 Monotonic이 아닌 케이스는 이 영상에서 잘 설명해주고 있습니다.
class를 구성하는 것들
디폴트 metaclass인 type, 그리고 user-defined class인 A는 아래와 같이 다양한 special method/attribute(s)를 가지고 있습니다 (user-defined class는 user-defined method/attribute(s)도 가짐).
type에 dir을 실행하면 type__dir__impl이 실행되며, type 생성 시 설정한 bases와 dict를 merge하여 key만 리턴해줍니다.
for attr in dir(type):
if attr == '__abstractmethods__':
continue
print(f"{attr:25}", type(getattr(type, attr)))
# 출력
__base__ <class 'type'>
__bases__ <class 'tuple'>
__basicsize__ <class 'int'>
__call__ <class 'wrapper_descriptor'>
__class__ <class 'type'>
__delattr__ <class 'wrapper_descriptor'>
__dict__ <class 'mappingproxy'>
__dictoffset__ <class 'int'>
__dir__ <class 'method_descriptor'>
__doc__ <class 'str'>
__eq__ <class 'wrapper_descriptor'>
__flags__ <class 'int'>
__format__ <class 'method_descriptor'>
__ge__ <class 'wrapper_descriptor'>
__getattribute__ <class 'wrapper_descriptor'>
__gt__ <class 'wrapper_descriptor'>
__hash__ <class 'wrapper_descriptor'>
__init__ <class 'wrapper_descriptor'>
__init_subclass__ <class 'builtin_function_or_method'>
__instancecheck__ <class 'method_descriptor'>
__itemsize__ <class 'int'>
__le__ <class 'wrapper_descriptor'>
__lt__ <class 'wrapper_descriptor'>
__module__ <class 'str'>
__mro__ <class 'tuple'>
__name__ <class 'str'>
__ne__ <class 'wrapper_descriptor'>
__new__ <class 'builtin_function_or_method'>
__prepare__ <class 'builtin_function_or_method'>
__qualname__ <class 'str'>
__reduce__ <class 'method_descriptor'>
__reduce_ex__ <class 'method_descriptor'>
__repr__ <class 'wrapper_descriptor'>
__setattr__ <class 'wrapper_descriptor'>
__sizeof__ <class 'method_descriptor'>
__str__ <class 'wrapper_descriptor'>
__subclasscheck__ <class 'method_descriptor'>
__subclasses__ <class 'method_descriptor'>
__subclasshook__ <class 'builtin_function_or_method'>
__text_signature__ <class 'NoneType'>
__weakrefoffset__ <class 'int'>
mro <class 'method_descriptor'>
User defined class A를 살펴보면,
class A:
class_a = 'a'
def name(self):
print('A')
def __init__(self):
self.instance_a = 'a'
for attr in dir(A):
print(f"{attr:25}", type(getattr(A, attr)))
#
__class__ <class 'type'>
__delattr__ <class 'wrapper_descriptor'>
__dict__ <class 'mappingproxy'>
__dir__ <class 'method_descriptor'>
__doc__ <class 'NoneType'>
__eq__ <class 'wrapper_descriptor'>
__format__ <class 'method_descriptor'>
__ge__ <class 'wrapper_descriptor'>
__getattribute__ <class 'wrapper_descriptor'>
__gt__ <class 'wrapper_descriptor'>
__hash__ <class 'wrapper_descriptor'>
__init__ <class 'function'>
__init_subclass__ <class 'builtin_function_or_method'>
__le__ <class 'wrapper_descriptor'>
__lt__ <class 'wrapper_descriptor'>
__module__ <class 'str'>
__ne__ <class 'wrapper_descriptor'>
__new__ <class 'builtin_function_or_method'>
__reduce__ <class 'method_descriptor'>
__reduce_ex__ <class 'method_descriptor'>
__repr__ <class 'wrapper_descriptor'>
__setattr__ <class 'wrapper_descriptor'>
__sizeof__ <class 'method_descriptor'>
__str__ <class 'wrapper_descriptor'>
__subclasshook__ <class 'builtin_function_or_method'>
__weakref__ <class 'getset_descriptor'>
class_a <class 'str'>
name <class 'function'>
A의 instance를 살펴보면,
a = A()
for attr in dir(a):
print(f"{attr:25}", type(getattr(a, attr)))
#
__class__ <class 'type'>
__delattr__ <class 'method-wrapper'>
__dict__ <class 'dict'>
__dir__ <class 'builtin_function_or_method'>
__doc__ <class 'NoneType'>
__eq__ <class 'method-wrapper'>
__format__ <class 'builtin_function_or_method'>
__ge__ <class 'method-wrapper'>
__getattribute__ <class 'method-wrapper'>
__gt__ <class 'method-wrapper'>
__hash__ <class 'method-wrapper'>
__init__ <class 'method'>
__init_subclass__ <class 'builtin_function_or_method'>
__le__ <class 'method-wrapper'>
__lt__ <class 'method-wrapper'>
__module__ <class 'str'>
__ne__ <class 'method-wrapper'>
__new__ <class 'builtin_function_or_method'>
__reduce__ <class 'builtin_function_or_method'>
__reduce_ex__ <class 'builtin_function_or_method'>
__repr__ <class 'method-wrapper'>
__setattr__ <class 'method-wrapper'>
__sizeof__ <class 'builtin_function_or_method'>
__str__ <class 'method-wrapper'>
__subclasshook__ <class 'builtin_function_or_method'>
__weakref__ <class 'NoneType'>
class_a <class 'str'>
instance_a <class 'str'>
name <class 'method'>
위에서 보듯이, type / object / instance에는 다양한 __로 시작하는 special methods들이 존재합니다. 그리고 그것들은 대략적으로 각 (cpython의) object -> ob_type -> tp_<이름>과 연결되어 실행됩니다 (wrapper_descriptor, builtin_function_or_method 등등)
이러한 special methods들은 파이썬의 특정 operation이나 내장함수 실행 시 사용됩니다.
예시로, 아래와 같이 수정한 metaclass를 사용하여 instance를 생성했을 때, 관여하는 special methods를 확인할 수 있습니다.
class Meta(type):
def __prepare__(mcs, name):
print('prepare')
return {}
def __new__(mcs, name, bases, attrs, **kwargs):
print('new')
return super().__new__(mcs, name, bases, attrs)
def __call__(cls, *args, **kwargs):
print('call')
return super().__call__(*args, **kwargs)
class A(metaclass=Meta):
def __init__(self):
print('init')
# 실행 시 출력
prepare
new
a = A()
#
call
init
function과 method (classmethod, staticmethod)
클래스 내부의 함수들의 종류를 좀 더 자세히 살펴보면 다음과 같습니다:
class C(object):
def f1(self, val):
return val
@staticmethod
def fs():
pass
@classmethod
def fc(cls):
return cls
print(C.f1)
print(C.fs)
print(C.fc)
#
<function C.f1 at 0x7f26c1442e18>
<function C.fs at 0x7f26c1442bf8>
<bound method C.fc of <class '__main__.C'>>
c = C()
print(c.f1)
print(c.fs)
print(c.fc)
#
<bound method C.f1 of <__main__.C object at 0x7f26c1401e48>>
<function C.fs at 0x7f26c1442bf8>
<bound method C.fc of <class '__main__.C'>>
먼저 클래스 C에 대해 살펴보면, f1과 fs는 function 그리고 @classmethod가 씌워진 fc는 bound method라는 것을 볼 수 있습니다. 클래스도 metaclass의 instance이기에, 그러한 클래스에 의존한 method를 생성하였습니다.
C 클래스 - c 인스턴스를 비교해보면 f1이 function에서 bound method로 변경된 것을 볼 수 있습니다. 그렇기에 아무 annotation이 없는 함수는 인스턴스 namespace에서 bound된다는 것을 알 수 있습니다.
cpython에서 functions과 methods는 아래와 같은 구조를 가지고 있습니다:
위에서 나온, function과 bound method를 중점적으로 살펴보면, function의 경우 closure, global, module 등등 context를 직접 가지고 있는 반면 bound method는 func, self 등으로 의존하고 있는 것을 볼 수 있습니다:
set(dir(C.fs)) - set(dir(C.fc))
# function만 가지고 있는 것
{'__annotations__',
'__closure__',
'__code__',
'__defaults__',
'__dict__',
'__globals__',
'__kwdefaults__',
'__module__',
'__name__',
'__qualname__'}
set(dir(C.fc)) - set(dir(C.fs))
# bound method만 가지고 있는 것
{'__func__', '__self__'}
class / instance 생성
파이썬의 클래스와 인스턴스 생성은 동일하게 ()인 call(CALL_FUNCTION)을 실행하며 진행됩니다.
아래와 같이 dis 모듈을 통해 클래스, 인스턴스 생성 시의 op_code를 확인할 수 있습니다:
from dis import dis
dis('class A:\n\tpass')
1 0 LOAD_BUILD_CLASS
2 LOAD_CONST 0 (<code object A at 0x7efe48eecdb0, file "<dis>", line 1>)
4 LOAD_CONST 1 ('A')
6 MAKE_FUNCTION 0
8 LOAD_CONST 1 ('A')
10 CALL_FUNCTION 2
12 STORE_NAME 0 (A)
14 LOAD_CONST 2 (None)
16 RETURN_VALUE
dis('A()')
1 0 LOAD_NAME 0 (A)
2 CALL_FUNCTION 0
4 RETURN_VALUE
2가지의 생성은 전반적으로 비슷한 flow를 공유하면서 부분부분 다른 값과 로직을 통해 진행됩니다:
- 클래스의 생성은 __build_class__ 부분의 값 설정이 선행합니다.
- __call__을 실행해 얻은 변수 type은 type_call 함수의 인자로 전달됩니다.
- type -> tp_new를 실행하여 obj를 얻고 obj가 type의 subtype인지 여부에 따라 리턴값이 결정됩니다.
Reference
[2] CPython Internals - Class
[3] CPython Internals - type
[4] Fluent Python
[5] Inside the Python Virtual Machine
[6] Python class inherit object
[8] blog.ionelmc.ro/2015/02/09/understanding-python-metaclasses/
[9] Method Resolution Order in Python
[10] PEP-575