[Python] Metaclass: type, __init_subclass__
정의
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)
ModelBase가 Field 인스턴스를 스캔해 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) 진가를 발휘한다.
관련 위키
- python - Python 언어 개요
- py-class-basics - 클래스 기초,
__init__,__new__ - py-inheritance - 다중 상속, MRO (C3 선형화)
- py-descriptor -
__get__,__set__,__set_name__ - py-decorator - 클래스 데코레이터
- py-collections-abc - ABCMeta, abstractmethod, Protocol
- py-enum - EnumMeta 활용
- py-dataclass -
@dataclass(metaclass 없이 데이터 클래스) - py-typeddict-protocol - Protocol, TypedDict
- py-property - property descriptor
이 글의 용어 (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년 발표한 인터프리터 방식의 동적 타이핑 언어다. 읽기 쉬운 문법과 방대한 라이브러리 생태계로 웹 백엔드, 데이터 분석…
💬 댓글