-
[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 매직 메소드를 잘 이해하고 있는 것이 중요합니다.
일반적으로 생성에 사용되는 매직 메소드의 호출 순서는 new → init → call 순서로 호출이 됩니다.
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 메소드가 호출됩니다.
이제 싱글톤 패턴에서 왜 new 나 init이 아닌 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들을 각 용도에 맞게 타입을 변환해 줍니다.
요약
메타클래스를 사용하면 클래스의 데이터를 원하는대로 조작할 수 있지만 복잡하기 때문에 클래스 데코레이터와 비교하여 잘 사용해야 합니다.