디스크립터 편에서 이어진다.
그러면 어떻게 장고 모델에 필드로 작성했을때만 models.CharField(blank=True, max_length=31) 이 코드가 <django.db.models.fields.CharField>가 아니라 <django.db.models.query_utils.DeferredAttribute object at 0xffff915be2c0>로
변환이 되는걸까?
Model의 구현체를 보자
class Model(AltersData, metaclass=ModelBase):
def __init__(self, *args, **kwargs):
개발하면서 가끔 보는 metaclass라는 인자가 있다. 대체 어떤 기능을 하는걸까?
class ModelBase(type):
"""Metaclass for all models."""
def __new__(cls, name, bases, attrs, **kwargs):
super_new = super().__new__
type을 상속받아서 구현하는 것을 볼 수 있다. 그리고 new라는 매직메소드도 보인다.
type은 우리가 그냥 특정 오브젝트의 타입, 클래스를 체크하는 용도로 쓰인게 아니였나?
우리가 평소에 사용하는 용도는 type(obj) 처럼 이제 런타임 중 특정 오브젝트의 타입, 클래스 등을 체크하는데 쓰기도한다.
그런데 type은 다른 용도로도 사용할 수 있다.
——
def speak(self):
print(f"name : {self.name}")
def init(self, name):
self.name = name
DynamicClass = type(
"DynamicClass",
(), # 상속받을 클래스
{
"__init__": init,
"speak": speak,
"VERSION": 1.0
}
)
obj = DynamicClass("test")
obj.speak()
print(obj.VERSION)
print(obj)
name : test
1.0
<__main__.DynamicClass object at 0x102896fd0>
이처럼 런타임 환경에서 원하는 클래스를 동적으로 생성할 수 있다. 이는 메타프로그래밍 기법이 가능하게 하는 동작이기도 하다.
(메타프로그래밍 : 프로그램 런타임 중에 동적으로 메소드, 클래스 등을 추가하여 프로그램의 동작을 확장하고 조작할 수 있는 기법)
그런데 이렇게 클래스를 생성하는것이 아니라
class AClass(type):
처럼 클래스를 생성한다면 뭔가 다른 동작을 하는것인가?
아까 보았던 new 같은 함수는 어떤 동작을 하는걸까?
class MyMeta(type):
@classmethod
def __prepare__(mcs, name, bases, **kwargs):
print(f"[1] __prepare__ 호출 - name={name}, bases={bases}, kwargs={kwargs}")
return {}
def __new__(mcs, name, bases, namespace, **kwargs):
print(f"[2] __new__ 호출 - name={name}, bases={bases}, kwargs={kwargs}")
print(f"namespace keys={list(namespace.keys())}")
print(namespace)
cls_obj = super().__new__(mcs, name, bases, namespace)
print("cls_obj", cls_obj)
return cls_obj
def __init__(cls, name, bases, namespace, **kwargs):
print(f"[3] __init__ 호출 - cls={cls}, name={name}, bases={bases}, kwargs={kwargs}")
super().__init__(name, bases, namespace)
def __call__(cls, *args, **kwargs):
print(f"[4] __call__ 호출 - cls={cls}, args={args}, kwargs={kwargs}")
instance = super().__call__(*args, **kwargs)
return instance
print("class create")
class MyClass(metaclass=MyMeta):
VERSION = 1.0
def __init__(self, x):
print(f"MyClass.__init__ called with x={x}")
self.x = x
def test(self):
print("MyClass.test called")
print("instance create")
obj = MyClass(10)
obj.test()
class create
[1] __prepare__ 호출 - name=MyClass, bases=(), kwargs={}
[2] __new__ 호출 - name=MyClass, bases=(), kwargs={}
namespace keys=['__module__', '__qualname__', 'VERSION', '__init__', 'test']
{'__module__': '__main__', '__qualname__': 'MyClass', 'VERSION': 1.0, '__init__': <function MyClass.__init__ at 0x1028818a0>, 'test': <function MyClass.test at 0x102881940>}
cls_obj <class '__main__.MyClass'>
[3] __init__ 호출 - cls=<class '__main__.MyClass'>, name=MyClass, bases=(), kwargs={}
instance create
[4] __call__ 호출 - cls=<class '__main__.MyClass'>, args=(10,), kwargs={}
MyClass.__init__ called with x=10
MyClass.test called
메타클래스의 각 메소드는 동작 방식이 다음과 같다
위의 방법을 보면 클래스 생성시 각 변수 및 메소드를 제어 및 추가 동작을 할 수 있는 것을 알 수 있다. type은 클래스 객체를 만드는 클래스로서 파이썬의 모든 객체에서 class를 추적해보면 그 끝은 type이고 type의 class를 봐도 type이다.
바로 이 방법을 통해서 Model 클래스를 이용해서 객체를 생성 시 각 필드에 대해서 DeferredAttribute로 변환해주는 작업이 진행된다.
굳이 바로 디스크립터로 만드는게 아니라 메타클래스를 통해서 디스크립터로 변환하는것은 해당 필드 객체가 생성될 때 어느 모델에 붙을지 모르기 때문이다. 필드가 ORM으로 온전히 동작하려면 모델명, 앱 라벨, 필드명 등이 필요한데 그것은 필드 객체가 정의될 때 전부 알기 힘들기 때문으로 보인다.