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

[Python] Metaclass: type, __init_subclass__

· 수정 · 📖 약 2분 · 987자/단어 #python #metaclass #oop #type #advanced #abc #django
python metaclass, type metaclass, __init_subclass__, metaclass conflict, ABCMeta, ModelBase, 클래스 생성 프로토콜

정의

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는 정말 필요할 때만.

클래스 생성 프로토콜

class 문이 실행될 때 Python이 수행하는 단계.

flowchart TD
    A["class Foo(Base, metaclass=Meta): ..."] --> B["metaclass 결정\n(Base.__class__ 또는 metaclass= 인수)"]
    B --> C["Meta.__prepare__ 호출\n(네임스페이스 딕셔너리 생성)"]
    C --> D["클래스 본문 실행\n(def, 속성 할당 등 namespace 채움)"]
    D --> E["Meta.__new__ 호출\n(클래스 객체 생성)"]
    E --> F["Meta.__init__ 호출\n(클래스 객체 초기화)"]
    F --> G["descriptor __set_name__ 호출"]
    G --> H["Base.__init_subclass__ 호출"]
    H --> I["Foo 클래스 완성"]

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__는 클래스가 만들어질 때 한 번 호출. 클래스 데코레이터와 비슷하지만 서브클래스에도 자동 전파된다는 차이.

metaclass 메서드의 호출 시점

class Meta(type):
    @classmethod
    def __prepare__(mcs, name, bases, **kwargs):
        # 1. 클래스 본문을 담을 네임스페이스 딕셔너리 반환
        # OrderedDict 등 커스텀 딕셔너리를 반환하면 본문 순서 보존
        return super().__prepare__(name, bases, **kwargs)

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

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

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

__init_subclass__ (PEP 487, 3.6+)

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

class Plugin:
    registry: dict[str, type] = {}

    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 '__main__.AuthPlugin'>, 'CachePlugin': <class '__main__.CachePlugin'>}

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

키워드 인수로 데이터 전달

class Validated:
    def __init_subclass__(cls, *, max_size: int = 100, **kwargs):
        super().__init_subclass__(**kwargs)
        cls._max_size = max_size

    def validate(self, data):
        if len(data) > self._max_size:
            raise ValueError(f"Size exceeds {self._max_size}")

class SmallBuffer(Validated, max_size=10): pass
class LargeBuffer(Validated, max_size=10000): pass

print(SmallBuffer._max_size)   # 10
print(LargeBuffer._max_size)   # 10000

실제 metaclass 사용 사례

1. ABCMeta (abc.ABC 내부)

from abc import ABC, ABCMeta, abstractmethod

class Shape(ABC):   # ABC = class ABC(metaclass=ABCMeta)
    @abstractmethod
    def area(self) -> float: ...

    @abstractmethod
    def perimeter(self) -> float: ...

class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def area(self) -> float:
        import math
        return math.pi * self.radius ** 2

    def perimeter(self) -> float:
        import math
        return 2 * math.pi * self.radius

# Shape() -> TypeError: Can't instantiate abstract class
# Circle() -> OK (abstractmethod 모두 구현)
c = Circle(5)
print(f"area={c.area():.2f}")

ABCMeta__new__@abstractmethod로 마킹된 메서드를 수집해 __abstractmethods__ frozenset에 저장. type.__call__이 이 집합이 비어있지 않으면 TypeError.

자세히: py-collections-abc

2. Django ORM ModelBase

# Django 내부 (단순화)
class ModelBase(type):
    def __new__(mcs, name, bases, attrs):
        # 1. 클래스 속성에서 Field 인스턴스 수집
        new_class = super().__new__(mcs, name, bases, attrs)
        fields = {k: v for k, v in attrs.items()
                  if isinstance(v, Field)}
        # 2. _meta (Options 객체) 초기화
        new_class._meta = Options(new_class, fields)
        # 3. Manager 생성 (objects)
        new_class.objects = Manager(new_class)
        return new_class

class Model(metaclass=ModelBase):
    pass

# 사용자 코드
class User(models.Model):          # ModelBase.__new__ 호출
    name = models.CharField()
    email = models.EmailField()
# User._meta.fields = {'name': CharField, 'email': EmailField}
# User.objects = Manager(User)

ModelBaseField 인스턴스를 스캔해 SQL 컬럼 메타데이터 구성, Manager 자동 생성. class User(models.Model): 선언만으로 ORM이 작동하는 이유.

3. Singleton

class Singleton(type):
    _instances: dict = {}

    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):
    def __init__(self, env: str = "production"):
        self.env = env

a = Config("dev")
b = Config("staging")   # 무시됨, 이미 생성된 인스턴스 반환
print(a is b)    # True
print(a.env)     # dev

metaclass의 __call__은 클래스 호출(Config(...)) 자체를 가로챔.

4. Enum

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

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

print(Color.RED)           # Color.RED
print(Color(2))            # Color.GREEN
print(Color["BLUE"])       # Color.BLUE
print(type(Color))         # <class 'EnumType'>

자세히: py-enum

descriptor와 metaclass

__set_name__은 클래스 생성 시(metaclass 처리 후) 각 descriptor에 호출된다.

class Typed:
    def __set_name__(self, owner, name):
        self.name = name        # 'age' 등 필드명 자동 설정

    def __set__(self, obj, value):
        if not isinstance(value, int):
            raise TypeError(f"{self.name} must be int")
        obj.__dict__[self.name] = value

class Person:
    age = Typed()   # __set_name__ 호출: self.name = 'age'

p = Person()
p.age = 25   # OK
p.age = "25" # TypeError: age must be int

자세히: py-descriptor

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:
#   the metaclass of a derived class must be a (non-strict) subclass
#   of the metaclasses of all its bases

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

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

실제로는 ABCMeta와 사용자 metaclass 충돌이 흔한 상황. ABCMeta를 상속하거나 __init_subclass__로 대체해 충돌 자체를 피하는 게 낫다.

함정

metaclass가 적용될 때

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

class Base(metaclass=Meta):    # "Meta.__new__: Base"
    pass

class Sub(Base):               # "Meta.__new__: Sub" (서브클래스도!)
    pass

WARNING

metaclass는 해당 클래스뿐 아니라 모든 서브클래스에도 자동 적용된다. 서드파티 라이브러리 클래스를 상속할 때 예상치 못한 metaclass가 실행될 수 있다.

__prepare__ 리턴값의 함정

class OrderedMeta(type):
    @classmethod
    def __prepare__(mcs, name, bases, **kwargs):
        from collections import OrderedDict
        return OrderedDict()   # 3.7+ dict는 이미 순서 보장이라 불필요

    def __new__(mcs, name, bases, namespace, **kwargs):
        # namespace가 OrderedDict임
        return super().__new__(mcs, name, bases, dict(namespace))

Python 3.7부터 일반 dict가 삽입 순서를 보존하므로 OrderedDict 반환은 불필요.

클래스 데코레이터로 충분한 경우

# metaclass 불필요
class Meta(type):
    def __new__(mcs, name, bases, ns):
        ns["created"] = True
        return super().__new__(mcs, name, bases, ns)

# 클래스 데코레이터로 동일 효과 (더 단순)
def add_created(cls):
    cls.created = True
    return cls

@add_created
class MyClass: pass

CAUTION

클래스 데코레이터로 해결되면 metaclass 대신 데코레이터를 사용하라. metaclass는 서브클래스 전파가 필요하거나, __prepare__로 네임스페이스를 제어해야 할 때, 또는 ORM처럼 클래스 본문 파싱이 필요할 때만 정당화된다.

언제 쓰면 안 되는가

요구사항metaclass 대신
단순 데이터 클래스@dataclass
추상 메서드 강제abc.ABC
서브클래스 등록__init_subclass__
클래스 속성 변환클래스 데코레이터
Singleton모듈 변수, __new__ 오버라이드
인터페이스 검사 (정적)typing.Protocol

라이브러리 제작자가 사용자의 클래스 선언 문법을 마법처럼 변환할 때(ORM, 직렬화, DSL) 진가를 발휘한다.

관련 위키

이 글의 용어 (10개)
[Python] @property, getter, setter, deleterpython
정의 는 메서드를 속성처럼 접근할 수 있게 해주는 데코레이터다. 호출자는 처럼 보지만 내부적으로는 함수가 실행되어 계산되거나 검증된 값을 반환한다. Java의 getter/set…
[Python] 데코레이터: @decorator, functools.wraps, 클래스 데코레이터python
정의 데코레이터(decorator)는 함수/클래스를 받아 다른 함수/클래스를 반환하는 callable이다. 문법은 단순한 호출 변환에 불과: 기본 데코레이터 는 원래 함수의 행동…
[Python] 상속, super, MRO, ABCpython
단일 상속 super() 부모 메서드를 호출하려면 . 다중 상속에서 MRO를 따라 다음 클래스로 디스패치. 인자 없이 호출하면 현재 클래스/인스턴스가 자동 주입된다(Python …
[Python] class 기초: 정의, __init__, selfpython
클래스 정의 self Python 메서드의 첫 번째 인수는 인스턴스 자신이다. 관례적으로 라 부른다(언어가 강제하진 않지만 PEP 8 권장). 호출 시 는 내부적으로 로 바인딩된…
[Python] collections.abc: 추상 컬렉션 인터페이스python
정의 는 컬렉션의 추상 베이스 클래스를 제공한다. 사용자 정의 컬렉션이 빌트인과 같은 인터페이스를 구현했음을 명시하거나, isinstance 검사에 사용. PEP 3119. 타입…
[Python] dataclass: 자동 생성 메서드를 갖춘 데이터 클래스python
정의 (3.7+, PEP 557)는 클래스에 , , 등 상용구 메서드를 자동 생성해주는 데코레이터다. 데이터 컨테이너 클래스를 한 줄 데코레이터 + 필드 어노테이션만으로 만들 수…
[Python] Descriptor: __get__, __set__, __delete__python
정의 Descriptor는 , , 중 하나 이상을 구현한 클래스다. 다른 클래스의 클래스 속성으로 사용되면 그 속성 접근이 descriptor의 메서드를 통해 라우팅된다. , ,…
[Python] enum: Enum, IntEnum, Flag, StrEnumpython
정의 은 고정된 이름-값 매핑을 정의하는 표준 모듈. 마법 숫자나 문자열 상수를 타입화된 멤버로 대체해 타입 안정성·가독성·IDE 지원을 얻는다. Enum 기본 멤버는 싱글톤. …
[Python] TypedDict, Protocol, runtime_checkablepython
TypedDict (PEP 589, 3.8+) dict의 키와 값 타입을 명시. JSON API 응답이나 설정처럼 dict로 들어오지만 스키마가 정해진 경우. 런타임엔 그냥 di…
Pythonpython
정의 Python은 Guido van Rossum이 1991년 발표한 인터프리터 방식의 동적 타이핑 언어다. 읽기 쉬운 문법과 방대한 라이브러리 생태계로 웹 백엔드, 데이터 분석…

이 개념을 다룬 위키 페이지 (1)

💬 댓글

사이트 검색 / 명령어

검색

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