메타클래스

teal·2025년 2월 27일
0

Python

목록 보기
12/12

디스크립터 편에서 이어진다.


그러면 어떻게 장고 모델에 필드로 작성했을때만 models.CharField(blank=True, max_length=31) 이 코드가 <django.db.models.fields.CharField>가 아니라 <django.db.models.query_utils.DeferredAttribute object at 0xffff915be2c0>
변환이 되는걸까?

metaclass

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

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

메타클래스의 각 메소드는 동작 방식이 다음과 같다

  1. 클래스 생성 시 네임스페이스 준비(prepare)
  2. 클래스 생성 시 클래스 객체 생성(new)
  3. 클래스 생성 시 클래스 객체의 초기화 진행(init)
  4. 생성된 클래스 호출 시(객체 생성시, call)

위의 방법을 보면 클래스 생성시 각 변수 및 메소드를 제어 및 추가 동작을 할 수 있는 것을 알 수 있다. type은 클래스 객체를 만드는 클래스로서 파이썬의 모든 객체에서 class를 추적해보면 그 끝은 type이고 type의 class를 봐도 type이다.

바로 이 방법을 통해서 Model 클래스를 이용해서 객체를 생성 시 각 필드에 대해서 DeferredAttribute로 변환해주는 작업이 진행된다.

굳이 바로 디스크립터로 만드는게 아니라 메타클래스를 통해서 디스크립터로 변환하는것은 해당 필드 객체가 생성될 때 어느 모델에 붙을지 모르기 때문이다. 필드가 ORM으로 온전히 동작하려면 모델명, 앱 라벨, 필드명 등이 필요한데 그것은 필드 객체가 정의될 때 전부 알기 힘들기 때문으로 보인다.

profile
고양이를 키우는 백엔드 개발자

0개의 댓글