-
파이썬 클래스 내부구조 (Python Class Internals)Python 2021. 1. 25. 22:22반응형
클래스(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
반응형'Python' 카테고리의 다른 글
프로그래밍 언어와 파이썬 (2) 2021.05.19 파이썬 int 내부구조 (Python int Internals) (0) 2021.02.22 파이썬 리스트 내부구조 (Python List Internals) (0) 2021.02.13 파이썬으로 구글 시트 생성 후 다른 유저에게 공유하기 (0) 2021.01.16 파이썬 튜플 내부구조 (Python Tuple Internals) (0) 2021.01.12 파이썬 딕셔너리 내부구조와 관련 개념 살펴보기 (Python Dictionary Internals and relating concepts) (1) 2021.01.04 파이썬 딕셔너리 루프 - 파이썬 딕셔너리 순회하기 (Python Dictionary Iteration) (0) 2021.01.01 코딩초보 파이썬(Python) 공부법, 공부자료 (파이썬입문, 파이썬강좌) (0) 2020.11.23