Python

파이썬 클래스 내부구조 (Python Class Internals)

Kaden Sungbin Cho 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).

 

Part of Python 'type's tp slots - Image from [7]

 

그러한 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인 것을 알 수 있습니다.

 

Image from Author

그렇기에 (하나의 예시인) 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의 관계를 나타낼 수 있습니다:

 

Image from Author inspired by [8]

그렇기에  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인 것을 알 수 있습니다:

Image from Author inspired by [9]

 

아래의 클래스 D에서는 MRO가 D -> C -> A -> B이기에 process()를 실행하면, C의 process()가 실행되는 것을 확인할 수 있습니다:

Image from Author inspired by [9]

 

마지막 fail 케이스인 아래 이미지에서 C의 MRO 계산을 생각해보면,

 

C -> A -> B -> A였다가, 2번째 A는 후행하는 리스트에 존재하여 good candidate이 아니게 됩니다.

2번째 A를 제외한, C -> B -> A를 시도하면 C -> A -> B 인지 C -> B -> A인지 우선순위를 알 수 없게 됩니다. 그렇기에 아래와 같은 TypeError를 일으킵니다. 이렇게 Monotonic이 아닌 케이스는 이 영상에서 잘 설명해주고 있습니다.

Image from Author inspired by [9]

 

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는 아래와 같은 구조를 가지고 있습니다:

Image from Author inspired by [10]

위에서 나온, 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인지 여부에 따라 리턴값이 결정됩니다.

Image from Author inspired by [3, 8]

 

 

 

 

Reference

[1] Python Docs Classes

[2] CPython Internals - Class

[3] CPython Internals - type

[4] Fluent Python

[5] Inside the Python Virtual Machine

[6] Python class inherit object

[7] Python type tp slots

[8] blog.ionelmc.ro/2015/02/09/understanding-python-metaclasses/

[9] Method Resolution Order in Python

[10] PEP-575

반응형