Home [Python] 메타클래스(Metaclass)
Post
Cancel

[Python] 메타클래스(Metaclass)

본문은 참고한 블로그 및 자료를 제가 이해하기 쉽게 다시 정리한 글임을 밝힙니다.


1. 메타클래스(Metaclass)

파이썬에서는 모든 것이 객체이기 때문에 클래스도 그 자체로 객체이다. 그렇다면 클래스라는 객체를 생성하는 클래스는 무엇일까? 이처럼 “클래스의 클래스”메타클래스(metaclass)라고 한다.

파이썬에서 기본적으로 사용하는 메타클래스는 type이다.

type은 우리가 어떤 자료형의 타입을 알아내기 위해 사용하는 type()이 맞다!

1
2
3
4
5
class MyClass:
    pass
    
type(MyClass)
# <class 'type'>


1-1. type 활용하기

이러한 메타클래스 type을 활용하는 방법 두 가지를 살펴보자.

  1. 클래스 만들기
    • () = 상속 클래스 정의를 담는 tuple
    • {} = 클래스에서 정의할 속성과 메서드를 담는 dict
    1
    2
    3
    
    MyClass = type('MyClass', (), {})
    MyClass
    # <class '__main__.MyClass'>
    
  2. 커스텀 메타클래스 만들기

    type을 상속받아 커스텀 메타클래스를 정의하고, 이를 적용할 클래스의 metaclass 파라미터로 지정하면 된다. 이러한 커스텀 메타클래스를 통해서 원하는 방법으로 클래스가 동작하도록 제어할 수 있다.

    예를 들어 클래스 MyClass가 생성될 때 속성 a가 반드시 정수가 되어야 하는 상황을 가정해보자.

    이러한 검증 과정을 MyMetaClass__new__ 메서드에 명시하면 된다.

    1
    2
    3
    4
    5
    6
    7
    
    class MyMetaclass(type):
        def __new__(cls, clsname, bases, dct):
            assert type(dct['a']) is int, 'Attribute `a` is not an integer.'
            return type.__new__(cls, clsname, bases, dct)
       
    class MyClass(metaclass=MyMetaclass):
        a = 3.14
    

    실행 결과, 다음과 같이 assert 문이 실행되는 것을 확인할 수 있다.

    1
    2
    3
    4
    5
    6
    7
    
    Traceback (most recent call last):
      File "/Users/.../test.py", line 7, in <module>
        class MyClass(metaclass=MyMetaclass):
      File "/Users/.../test.py",, line 4, in __new__
        assert type(dct['a']) is int, 'Attribute `a` is not an integer.'
               ^^^^^^^^^^^^^^^^^^^^^
    AssertionError: Attribute `a` is not an integer.
    


2. 메서드 관점에서 살펴본 인스턴스 생성 내부 동작

2-1. 클래스

메서드 이름동작
__new__ 메서드클래스의 인스턴스를 생성한다. (메모리 할당)
__init__ 메서드생성된 인스턴스를 초기화한다.
__call__ 메서드인스턴스를 실행한다.

__init__ 메서드는 생성자가 아니라는 것에 주의해야 한다! 실제 생성은 __new__ 메서드에서 수행하며, __init__ 메서드에서는 생성된 인스턴스를 초기화할 뿐이다.


2-2. 메타클래스

메타클래스 또한 __new__, __init__, __call__ 메서드를 가지고 있다. 예제를 통해 동작 양상을 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class MyMetaClass(type):
    def __new__(cls, *args, **kwargs):
        print('metaclass __new__')
        return super().__new__(cls, *args, **kwargs)

    def __init__(cls, *args, **kwargs):
        print('metaclass __init__')
        super().__init__(*args, **kwargs)

    def __call__(cls, *args, **kwargs):
        print('metaclass __call__')
        return super().__call__(*args, **kwargs)

class MyClass(metaclass=MyMetaClass):
    def __new__(cls, *args, **kwargs):
        print('child __new__')
        obj = super().__new__(cls)
        return obj

    def __init__(self):
        print('child __init__')

    def __call__(self):
        print('child __call__')

print('=============================================')
print('---- Instantiate MyClass:')
obj = MyClass()
print('---- Call MyClass Instance:')
obj()

print('=============================================')
print(f'{type(MyClass)=}')
1
2
3
4
5
6
7
8
9
10
11
metaclass __new__
metaclass __init__
=============================================
---- Instantiate MyClass:
metaclass __call__
child __new__
child __init__
---- Call MyClass Instance:
child __call__
=============================================
type(MyClass)=<class '__main__.MyMetaClass'>


위의 코드에서 알 수 있는 점은 다음과 같다.

  1. MyClass의 타입은 MyMetaClass이다.
  2. 메타클래스 MyMetaClass를 상속한 클래스 MyClass의 정의가 로드되는 순간, 이미 MyMetaClass는 생성 및 초기화 된다. 따라서 MyMetaClass__new__, __init__ 메서드가 차례로 호출된다.

    MyClass가 로드되는 순간에 MyClass라는 객체가 생성된 것이고, 객체가 생성되었다는 것은 클래스의 생성자가 호출되었다는 의미이다. 따라서 MyClass를 instantiate 하기 전에 MyMetaClass는 instantiate 된다.

  3. 실제로 클래스 MyClass를 instantiate 할 때에는 MyMetaClass 인스턴스가 실행된다. 즉, 다음과 같이 실행되는 것이다.

    1
    
    MyClass() == (MyMetaClass())()
    

    따라서 MyMetaClass__call__ 메서드가 호출된다. 그리고 MyMetaClass__call__ 메서드에서는 인스턴스의 생성자 __new__ 메서드를 호출하기 때문에, MyClass의 인스턴스가 생성되는 것이다.


3. 활용 방안

앞서, 메타클래스를 통해서 원하는 방법으로 클래스가 동작하도록 제어할 수 있다고 했었다. 이때, 지금까지 살펴봤던 내용 중 떠올려야 할 메타클래스의 핵심 특징은 다음과 같다:

어떤 메타클래스를 상속받은 클래스를 instantiate 할 때마다, 메타클래스의 __call__ 메서드가 호출된다.

따라서 메타클래스의 __call__ 메서드에 어떤 클래스가 생성될 때마다 필요한 동작을 명시할 수 있는 것이다.

가장 대표적인 예시로 singleton 기능을 메타클래스를 통해 제공할 수 있다.

1
2
3
4
5
6
7
8
9
class SingletonType(type):
    def __call__(cls, *args, **kwargs):
        if not hasattr(cls, "_instance"):
            cls._instance = super(SingletonType, cls).__call__(*args, **kwargs)
        return cls._instance

class Foo(metaclass=SingletonType):
    def __init__(self, name):
        self.name = name
1
2
3
4
5
6
7
obj1 = Foo('name')
obj2 = Foo('name1')

print(obj1, obj2)
# <__main__.Foo object at 0x101646e90> <__main__.Foo object at 0x101646e90>
print(obj1 is obj2)
# True


References

This post is licensed under CC BY 4.0 by the author.

[Algorithm] Quick Select: K 번째 원소 찾기

[Python] Singleton을 사용하는 다섯 가지 방법