ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] metaclass란
    언어/파이썬 & 장고 2021. 7. 17. 19:08

    python에서는 클래스도 객체입니다. 그렇다면 클래스를 만드는 클래스가 있다는 얘기인데 이러한 역할을 하는 것이 metaclass(이하 메타클래스)입니다. 메타클래스를 사용하면 클래스를 만들 수 있게 됩니다. 보통의 상황에서는 잘 사용하진 않지만 Django의 Model 객체에서 사용하고 있는 것을 볼 수 있습니다.

    우리는 모르게 메타 클래스를 사용하고 있었는데 필드가 무슨 타입인지 확인할 때 사용하는 type()이 바로 메타클래스입니다. 즉, type()에는 타입이 무엇인지 와 메타클래스로서 클래스를 생성하는 2가지의 역할을 합니다.

    temp = type('aaa')
    temp2 = type('temp', (), {})
    temp3 = type(int)
    
    print(temp)
    print(temp2)
    print(temp3)
    
    # <class 'str'>
    # <class '__main__.temp'>
    # <class 'type'>

    위 첫 번째는 입력된 파라미터의 타입을 확인하는 역할이고 두 번째와 세 번째는 클래스를 생성한 케이스입니다.

    두 번째 temp라는 클래스를 생성했으니 이를 인스턴스까지 생성할 수 있습니다.

    temp2 = type('temp', (), {})
    
    temp2_class = temp2()
    
    print(temp2_class)
    
    # <__main__.temp object at 0x7fd8e811c370>

    더 나아가 type으로 temp 클래스를 생성할 때, 두 번째와 세 번째 파라미터로 튜플과 딕셔너리가 있는데 이를 표현하면 아래와 같습니다.

    class Temp(object):
        class_value = 100
    
        def add(self, a, b):
            return a + b
    
    _temp = Temp()
    print(_temp.add(1, 2))
    # 3
    
    temp2 = type('Temp2', (object,), {'class_value_2': 10, 'add': lambda self, a, b: a + b})
    _temp2 = temp2()
    print(_temp2.add(2, 3))
    # 5

    두 번째 인자인 튜플에는 부모 클래스로 어떤 값을 상속받을지 추가할 수 있습니다. 세 번째 딕셔너리엔 변수나 메소드를 추가할 수 있습니다.

    이와 같이 type을 사용해 클래스를 생성하는 이점으로는 동적으로 클래스를 만들 수 있다는 점입니다.

    다음으로 type을 상속받아 커스텀 메타클래스를 만들 수 있습니다. 이렇게 커스텀 메타클래스를 만들게 되면 이를 상속받는 클래스를 원하는 방향으로 생성할 수 있습니다.

    class CustomMetaclass(type):
        def __new__(cls, *args, **kwargs):
            class_name = args[0]
            parent_classes = args[1]
            properties = args[2]
    
            if properties.get('name') == 'Kim':
                raise ValueError('Kim은 안돼')
    
            return super().__new__(cls, args, kwargs)
    
    class Temp(object, metaclass=CustomMetaclass):
        name = 'Kim'
    
    temp = Temp()
    
    # Traceback (most recent call last):
    #   File ".py", line 13, in <module>
    #     class Temp(object, metaclass=CustomMetaclass):
    #   File ".py", line 8, in __new__
    #     raise ValueError('Kim은 안돼')
    # ValueError: Kim은 안돼

    위 코드에서 Temp 클래스는 metaclass로 CustomMetaclass를 받고 있습니다. 즉, Temp 클래스를 생성할 때, CustomMetaclass를 호출하게 됩니다. 여기서 new 메소드 내에 name이 Kim이라면 raise를 하도록 설정해 에러가 발생하는 것을 확인할 수 있습니다.

    아래는 커스텀 메타클래스를 활용하여 싱글톤 패턴을 구성한 예시입니다.

    class Singleton(type):
        # 클래스들의 인스턴스를 저장
        __instances = {}
    
        def __call__(cls, *args, **kwargs):
            """
            클래스를 인스턴스화 할 때, 호출
            """
    
            # __instances에 instance가 없으면 생성 후 반환, 존재하면 해당 값을 꺼내서 반환
            if cls not in cls.__instances:
                cls.__instances[cls] = super().__call__(*args, **kwargs)
            return cls.__instances[cls]
    
    class Util(metaclass=Singleton):
        pass
    
    class Request(metaclass=Singleton):
        pass
    
    util_1 = Util()
    util_2 = Util()
    
    request = Request()
    
    print(util_1 == util_2)
    print(util_1 == request)
    
    # True
    # False

    여기서 메타 클래스를 잘 사용하기 위해선 call, init, new 매직 메소드를 잘 이해하고 있는 것이 중요합니다.

    일반적으로 생성에 사용되는 매직 메소드의 호출 순서는 newinitcall 순서로 호출이 됩니다.

    new를 호출하면 해당 클래스를 인스턴스를 생성해 메모리에 저장시키고 init으로 해당 인스턴스를 초기화 해줍니다. call은 해당 인스턴스를 호출했을 때, 실행하는 역할입니다.

    다음 아래에서 호출의 예시를 볼 수 있습니다.

    class Temp:
        a = 111
    
        def __new__(cls, *args, **kwargs):
            print('Temp __new__() 호출')
            print(cls)
            instance = super().__new__(cls, *args, **kwargs)
            print(instance)
            return instance
    
        def __init__(self, *args, **kwargs):
            print('Temp __init__() 호출')
            print(self)
            super().__init__(*args, **kwargs)
    
        def __call__(self, *args, **kwargs):
            print('Temp __call__() 호출')
            print(self)
    
    temp = Temp
    print(temp.a)
    # 111
    
    temp2 = Temp()
    # Temp __new__() 호출
    # <class '__main__.Temp'>
    # <__main__.Temp object at 0x7ff418084850>
    # Temp __init__() 호출
    
    temp2()
    # Temp __call__() 호출
    # <__main__.Temp object at 0x7fbdf801c850>

    처음 인스턴스화 하지 않은 상태에서는 매직메소드들이 호출되지 않은 것을 볼 수 있습니다. 그 다음, 인스턴스화를 진행하면 new 메소드가 호출되고 인스턴스를 생성해 메모리에 저장한 다음, init을 호출해 초기화를 진행합니다. 이 때, new 메소드에서 생성한 인스턴스와 동일한 것을 볼 수 있습니다. 마지막으로 생성한 인스턴스를 다시 호출하게 되면 call 메소드가 호출됩니다.

    이제 싱글톤 패턴에서 왜 newinit이 아닌 call 메소드에 로직을 선언했는지 아래를 보면 명확해집니다.

    class Singleton(type):
        # 클래스들의 인스턴스를 저장
        __instances = {}
    
        def __new__(mcs, *args, **kwargs):
            print('메타클래스 __new__() 호출')
            print(mcs)
            instance = super().__new__(mcs, *args, **kwargs)
            print('인스턴스 생성', instance)
            print('--------------------')
            return instance
    
        def __init__(cls, *args, **kwargs):
            print('메타클래스 __init__() 호출')
            print(cls)
            print('--------------------')
            super().__init__(*args, **kwargs)
    
        def __call__(cls, *args, **kwargs):
            """
            클래스를 인스턴스화 할 때, 호출
            """
            print('메타클래스 __call__() 호출')
            print('--------------------')
            # __instances에 instance가 없으면 생성 후 반환, 존재하면 해당 값을 꺼내서 반환
            if cls not in cls.__instances:
                cls.__instances[cls] = super().__call__(*args, **kwargs)
            return cls.__instances[cls]
    
    class Util(metaclass=Singleton):
        def __new__(cls, *args, **kwargs):
            print('Util __new__() 호출')
            return super().__new__(cls, *args, **kwargs)
    
        def __init__(self, *args, **kwargs):
            print('Util __init__() 호출')
            super().__init__(*args, **kwargs)
    
        def __call__(self, *args, **kwargs):
            print('Util __call__() 호출')
    
    print('Util 인스턴스화 진행')
    print('=================')
    util_1 = Util()
    
    # 메타클래스 __new__() 호출
    # <class '__main__.Singleton'>
    # 인스턴스 생성 <class '__main__.Util'>
    # --------------------
    # 메타클래스 __init__() 호출
    # <class '__main__.Util'>
    # --------------------
    # Util 인스턴스화 진행
    # =================
    # 메타클래스 __call__() 호출
    # --------------------
    # Util __new__() 호출
    # Util __init__() 호출

    Util 클래스의 인스턴스가 생성되기 전에 메타 클래스의 new()와 init()이 호출된 것을 볼 수 있습니다. 여기서 맨 처음 한 설명을 다시 상기시키면 클래스는 객체이고 메타클래스는 클래스라는 객체를 만드는 클래스입니다. Util 클래스가 메모리에 저장되는 순간 Util이라는 객체(클래스)가 생성되었다고 볼 수 있습니다. 객체가 생성된 것은 클래스의 생성자가 호출되었다는 얘기이므로 Util 객체(클래스)의 클래스인 Singleton의 생성자가 호출된 것입니다. 다시 말해, Util 이라는 클래스는 Singleton의 인스턴스라고 볼 수 있습니다. 여기서 Util()이라고 호출하면 Singleton의 인스턴스를 호출하는 것이므로 Singleton 인스턴스의 call이 호출되게 됩니다.

    • Util() = Singleton()() 이 성립

    커스텀 메타클래스를 활용한 장고 Model

    장고 ORM은 이러한 메타클래스를 잘 사용한 예시라 볼 수 있습니다.

    class User(models.Model):
        name = fields.CharField()

    보통 모델을 정의할 때, 위와같이 진행하는데 User.objects.all[0].name 으로 데이터를 가져올 때 CharField() 타입으로 내려오는 것이 아닌 문자열로 내려오는 것을 볼 수 있습니다. 이 것이 가능한 이유로 Model에는 Modelbase라는 커스텀 메타클래스가 정의되어 있어 선언된 Field들을 각 용도에 맞게 타입을 변환해 줍니다.

    요약

    메타클래스를 사용하면 클래스의 데이터를 원하는대로 조작할 수 있지만 복잡하기 때문에 클래스 데코레이터와 비교하여 잘 사용해야 합니다.

    댓글