[Python] metaclass란
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들을 각 용도에 맞게 타입을 변환해 줍니다.
요약
메타클래스를 사용하면 클래스의 데이터를 원하는대로 조작할 수 있지만 복잡하기 때문에 클래스 데코레이터와 비교하여 잘 사용해야 합니다.