본문으로 건너뛰기
김신건의 로그

[Python] Metaclass: type, __init_subclass__

· 수정 · 📖 약 2분 · 671자/단어 #python #metaclass #oop #type #advanced
python metaclass, type metaclass, __init_subclass__, metaclass conflict

정의

Metaclass는 “클래스의 클래스”다. 일반 클래스가 인스턴스의 행동을 결정하듯, metaclass는 클래스 자체의 행동(생성, 속성, 메서드 추가)을 결정한다. 모든 클래스의 기본 metaclass는 type.

class C: pass
print(type(C))         # <class 'type'>
print(type(C()))       # <class 'C'>
print(type(type))      # <class 'type'> (type의 type은 자기자신)

“Metaclasses are deeper magic than 99% of users should ever worry about.” - Tim Peters

대부분 __init_subclass__나 클래스 데코레이터로 해결 가능하다. metaclass는 정말 필요할 때만.

type 활용 (간단)

type(name, bases, namespace)로 클래스를 동적 생성한다.

D = type("D", (), {"x": 1, "greet": lambda self: "hi"})
d = D()
print(d.x, d.greet())    # 1 hi

# 다음과 동등
class D:
    x = 1
    def greet(self): return "hi"

metaclass 정의

type을 상속해 metaclass 클래스를 만들고, 클래스 선언 시 metaclass= 인자로 지정.

class Logged(type):
    def __new__(mcs, name, bases, namespace):
        print(f"creating class {name}")
        return super().__new__(mcs, name, bases, namespace)

class User(metaclass=Logged):    # 클래스 생성 시 출력
    pass

class Admin(User):                # 출력
    pass

Logged.__new__는 클래스가 만들어질 때 한 번 호출. 클래스 데코레이터와 비슷하지만 서브클래스에도 자동 전파된다는 차이.

init_subclass (PEP 487, 3.6+)

대부분의 metaclass 용도는 __init_subclass__로 대체 가능. 더 간단하고 일반인 친화적.

class Plugin:
    registry = {}

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        Plugin.registry[cls.__name__] = cls

class AuthPlugin(Plugin): pass
class CachePlugin(Plugin): pass

print(Plugin.registry)
# {'AuthPlugin': <class 'AuthPlugin'>, 'CachePlugin': <class 'CachePlugin'>}

서브클래스가 정의될 때마다 부모의 __init_subclass__가 호출된다. 자동 등록, 인터페이스 검사, 클래스 변환 등 대부분의 metaclass 패턴을 커버.

실제 metaclass 사용 사례

1. ABCMeta (abc.ABC 내부)

from abc import ABCMeta, abstractmethod

class Shape(metaclass=ABCMeta):
    @abstractmethod
    def area(self): ...

@abstractmethod 검사와 가상 서브클래스 등록 메커니즘이 ABCMeta에 구현되어 있다.

2. Django Model

class User(models.Model):
    name = models.CharField()
    email = models.EmailField()

models.Model의 metaclass(ModelBase)가 클래스 본문을 스캔해 필드 메타데이터 수집, manager 생성, SQL 컬럼 매핑.

3. Singleton

class Singleton(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class Config(metaclass=Singleton):
    pass

a, b = Config(), Config()
print(a is b)    # True

metaclass의 __call__은 클래스 호출(Config(...)) 자체를 가로챔. __new__나 모듈 변수보다 더 정교한 제어.

4. Enum

enum.Enum의 metaclass EnumMeta가 클래스 본문의 모든 클래스 속성을 멤버로 변환하고, 인스턴스화를 막고, 이름·값 룩업을 구축.

metaclass 메서드의 호출 시점

class Meta(type):
    def __init_subclass__(mcs, **kwargs):
        # metaclass의 서브클래스가 정의될 때
        super().__init_subclass__(**kwargs)

    def __new__(mcs, name, bases, namespace, **kwargs):
        # 클래스 객체 생성 (가장 먼저)
        return super().__new__(mcs, name, bases, namespace)

    def __init__(cls, name, bases, namespace, **kwargs):
        # 클래스 객체 초기화 (__new__ 다음)
        super().__init__(name, bases, namespace)

    def __call__(cls, *args, **kwargs):
        # 클래스를 호출해 인스턴스 만들 때 (Cls(...))
        instance = super().__call__(*args, **kwargs)
        return instance

metaclass 충돌

다중 상속 시 metaclass가 호환되지 않으면 TypeError.

class M1(type): pass
class M2(type): pass

class A(metaclass=M1): pass
class B(metaclass=M2): pass

class C(A, B): pass
# TypeError: metaclass conflict

해결: 두 metaclass를 묶는 새 metaclass 정의.

class CombinedMeta(M1, M2): pass
class C(A, B, metaclass=CombinedMeta): pass

키워드 인수로 metaclass에 데이터 전달

PEP 487 이후 class C(Base, key=value) 키워드 인수는 __init_subclass__와 metaclass에 전달.

class Registered:
    def __init_subclass__(cls, *, prefix, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.prefix = prefix

class Item(Registered, prefix="item:"):
    pass

print(Item.prefix)    # 'item:'

디버깅 팁

  • 클래스 출력 시 metaclass 보임: class C(metaclass=MyMeta):MyMeta.__repr__ 호출 가능
  • inspect.getmro()로 MRO 확인
  • 신비한 동작은 metaclass / init_subclass / 데코레이터 / Mixin 어디에서나 올 수 있음. __class__.__mro__ 추적

언제 쓰면 안 되는가

  • 단순 데이터 클래스 → @dataclass
  • 추상 메서드 강제 → abc.ABC
  • 서브클래스 등록 → __init_subclass__
  • 클래스 속성 변환 → 클래스 데코레이터
  • Singleton → 모듈 변수, 함수, 또는 __new__

대부분 metaclass 없이 해결된다. 라이브러리 제작자가 사용자의 클래스 선언 문법을 마법처럼 변환할 때(ORM, 직렬화, DSL) 진가.

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기