Python의 클래스 시스템 및 매커니즘

maintain·2021년 8월 25일
0

시작하기 전에 짚고 갈 것

  • Python에서는 클래스 또한 객체의 일종으로 취급되며, type의 인스턴스입니다.
  • 내부적으로 type을 이용해 클래스가 생성되고, type을 이용해 런타임에 클래스를 선언하는 것도 가능합니다. 또한 type은 type 클래스의 인스턴스입니다.
  • 이 글은 CPython을 기준으로 작성되었습니다. 다른 구현에서는 다를 수도 있습니다.

순서

1. mro 상속 체인 생성

  • type의 인스턴스라면 해당 클래스의 MRO(Method Resolution Order)로, 아니라면 __mro_entries__ 메서드를 통해서 mro 상속 체인을 만듭니다.
  • 이 MRO(Method Resolution Order) 상속 체인은 super()를 사용할 때 접근하는 클래스의 순서와 같습니다.
  • 개인적으로 개발하는 프로젝트에서 발견한 (이 글을 쓰게 된 계기가 됩니다.) 재밌는 예시로 typing 모듈의 TypedDict 이 있습니다. 일반 dict 처럼 선언해서 사용가능하고, 특이하게 부모 클래스로 사용해서 선언할 수 있습니다. 런타임엔 일반 dict 입니다.
# 클래스처럼 선언, 이 클래스는 dict가 아님.
class NMT(TypedTuple):
    a: int
    b: int

type(NMT(a=1, b=1))  # 사용한 후에는 dict
  • 해당 클래스는 특이하게도 사실 함수고, __mro_entries__ 메서드를 활용해서 상속 체인을 구현하고 있습니다.

    코드 원본(python github)

def TypedDict(typename, fields=None, /, *, total=True, **kwargs):
    ns = {'__annotations__': dict(fields)}
    try:
        ns['__module__'] = sys._getframe(1).f_globals.get('__name__', '__main__')
    except (AttributeError, ValueError):
        pass

    return _TypedDictMeta(typename, (), ns, total=total)

_TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {})
TypedDict.__mro_entries__ = lambda bases: (_TypedDict,)

2. 클래스 내부 변수, 메서드가 컴파일됩니다.

  • 글로벌 컨텍스트, 클래스의 __qualname__ 을 기본 컨텍스트로 사용할 수 있습니다.
  • 메타클래스의 __prepare__ 훅에서 이 시점에 사용할 네임스페이스를 정의할 수 있습니다. 해당 메서드는 반드시 클래스메서드이어야 하며(메타 클래스의 인스턴스가 생성되기 전이므로 인스턴스가 존재하지 않습니다) 정의되지 않았다면 빈 OrderdDict입니다.
  • 위 네임스페이스는 곧 해당 클래스의 메서드나 변수가 됩니다.

3. 클래스 객체가 생성됩니다.

  • 일반 인스턴스가 초기화되는 과정과 같습니다. 다만 클래스로 바뀐 것 뿐입니다.
  • 메타클래스의 인스턴스가 새로 만들어집니다. (metaclass.__new__)
    • 메모리에 객체가 생성되는 시점입니다.
    • 해당 메서드는 암시적으로 스태틱메서드입니다. 첫 인자로 만들길 원하는 인스턴스의 클래스가 옵니다.
  • 이 시점 (__new__다음 __init__이전) 에 디스크립터의 __set_name__메서드가 실행됩니다.
  • 위에서 일반 인스턴스와 같은 과정을 거친다고 했는데, 그렇기 때문에 메타클래스의 변수나 메서드는 해당 메타클래스를 사용한 클래스의 클래스 변수나 클래스메서드가 됩니다.

4. 서브클래스 훅이 호출됩니다.

  • 이 과정은 엄밀하게는 위 생성(type.__new__)의 마지막 과정입니다.
  • MRO 체인의 순서를 따라 가장 가까운 부모의 __init_subclass__가 호출됩니다.
  • class.__init_sublcass__ 훅은 개인적으로는 별개의 메타클래스를 선언할 필요가 없기 때문에 특별한 이유가 없다면 가장 선호하는 방법입니다.

5. 클래스 객체가 초기화됩니다.

  • 일반적인 __init__훅을 통해 해당 인스턴스가 초기화됩니다. (metaclass.__init__)
  • 지금까지 순서를 확인해보면 다음과 같습니다.
class meta(type):
    def __new__(cls, *args):
        print('new1', cls)
        ret = super().__new__(cls, *args)
        print('new2', cls)
        return ret

    def __init__(self, *args, **kwargs):
        print('init1', self)
        super().__init__(*args, **kwargs)
        print('init2', self)
    
    @classmethod
    def __prepare__(cls, *args):
        print('prepare', cls)
        return {}


class A(metaclass=meta):
    def __init_subclass__(cls) -> None:
        print('init_sub', cls)

class B(A):
    pass

# 결과
prepare <class '__main__.meta'>  A 준비
new1 <class '__main__.meta'>  A 생성
new2 <class '__main__.meta'>  A 생성
init1 <class '__main__.A'>  A 초기화
init2 <class '__main__.A'>  A 초기화

prepare <class '__main__.meta'>  B 준비
new1 <class '__main__.meta'>  B 생성
init_sub <class '__main__.B'>  B 생성 마지막에 서브클래스 훅
new2 <class '__main__.meta'>  B 생성
init1 <class '__main__.B'>	B 초기화
init2 <class '__main__.B'>  B 초기화

5. 데코레이터가 처리됩니다.

  • 함수 데코레이터에서 데코레이터는 함수가 완전히 정의된 뒤에 실행되는 것처럼, 클래스 데코레이터도 동일합니다.

참조

https://docs.python.org/ko/3/reference/datamodel.html#metaclasses

https://seonghyeon.dev/realworld-metaclass

0개의 댓글