python metaclass

김도연
11 min readAug 8, 2021

--

파이썬은 모든게 객체다. 객체를 생성하는데 class가 사용되는데 class 또한 객체다. 그러면 class를 생성하는 class가 또 있어야하는걸까?

metaclass

파이썬에서 객체를 생성하는 class 객체를 만드는 특별한 객체가 있는데 그걸 metaclass 라고 한다. 그럼 기본적인 class의 metaclass는 뭘까

class SnowBall:
pass
# 스노우볼의 타입을 알아보자
$ type(SnowBall)
-> type

파이썬 클래스에서 기본적인 metaclass는 type이다. 객체의 타입을 알아볼 때 호출하는 그 함수가 맞다. SnowBall은 따로 metaclass를 설정하지 않았기 때문에 기본적인 metaclass인 type이 SnowBall이라는 클래스 객체를 만들어주었다. 아래처럼 type을 사용해 SnowBall을 만들어도 위와 동일한 클래스 객체가 만들어진다.

class SnowBall:
pass
type('SnowBall', (), {})# 위 두 개는 SnowBall이라는 클래스를 만드는 2 가지 방법이다.

custom metaclass

type을 상속받아서 직접 메타클래스를 만들 수 있다. 그리고 custom metaclass는 metaclass 키워드의 인자로 전달할 수 있다.

class BaseMeta(type):
pass
class SnowBall2(metaclass=BaseMeta):
pass
class SnowBallBall(SnowBall):
pass
# 타입을 알아보자
$ type(BaseMeta)
-> type
$ type(SnowBall2)
-> __main__.BaseMeta
$ type(SnowBallBall)
-> __main__.BaseMeta

__new__, __init__, __call__

내부적으로 3가지 메소드가 호출된다. 메타클래스로 클래스 인스턴스 생성시 __new__ 가 호출되고, 생성된 인스턴스를 초기화할 때 __init__이 실행이되고 __call__은 인스턴스 실행시 호출된다.

class BaseMeta(type):
def __new__(cls, *args, **kwargs):
print('metaclass __new__')
return super().__new__(cls, *args, **kwargs)

def __init__(cls, *args, **kwargs):
print('metaclass __init__')
super().__init__(*args, **kwargs)

def __call__(cls, *args, **kwargs):
print('metaclass __call__')
return super().__call__(*args, **kwargs)


class SnowBall(metaclass=BaseMeta):
def __init__(self):
print('SnowBall __init__')

def __call__(self):
print('SnowBall __call__')

print('zug-zug')
sb = SnowBall()
print('zug-zug-zug')
sb()
# 콘솔 출력 결과
metaclass __new__
metaclass __init__
zug-zug
metaclass __call__
SnowBall __init__
zug-zug-zug
SnowBall __call__

호출 순서를 살펴보자.
SnowBall을 선언하면 해당 SnowBall 클래스 객체를 BaseMeta 라는 메타클래스가 SnowBall이라는 클래스를 생성한다. 그래서 클래스 인스턴스 생성시 호출되는 __new__가 호출되고 생성된 클래스 객체(여기선 class SnowBall)을 초기화하기 위해 __init__이 호출되었다.

그리고 print 결과 이후 SnowBall 클래스로 sb라는 변수에 새로운 객체를 생성해서 할당했다. 새로운 객체 생성을 위해 BaseMeta의 인스턴스인 class SnowBall 이 실행되었이 때문에 metaclass __call__이 호출되고 SnowBall로 만든 새로운 객체를 초기화 시키기 위해 SnowBall __init__ 이 호출된 것이다.

마지막으로 sb를 실행하면 SnowBall의 인스턴스를 실행하는것이므로 SnowBall __call__ 이 호출된다.

실제로 메타클래스를 직접 구현해서 사용할 일은 별로 없을 것 같다.

“Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t (the people who actually need them know with certainty that they need them, and don’t need an explanation about why).”

Tim Peters (realpython)

실제로 어떻게 사용되나 django db model에서 자주 사용하는 TextChoices를 한번 따라가보았다.

# 자주 사용하는 TextChoices
class TextChoices(str, Choices):
"""Class for creating enumerated string choices."""

def _generate_next_value_(name, start, count, last_values):
return name
--------------------------------------------------------------------
# 부모클래스에서 metaclass를 상속한게 보인다 -> ChoicesMeta
class Choices(enum.Enum, metaclass=ChoicesMeta):
"""Class for creating enumerated choices."""

def __str__(self):
"""
Use value when cast to str, so that Choices set as model instance
attributes are rendered as expected in templates and similar contexts.
"""
return str(self.value)
--------------------------------------------------------------------
# ChoicesMeta는 EnumMeta라는 메타클래스를 상속했다!
class ChoicesMeta(enum.EnumMeta):
"""A metaclass for creating a enum choices."""
def __new__(metacls, classname, bases, classdict, **kwds):
labels = []
for key in classdict._member_names:
value = classdict[key]
if (
isinstance(value, (list, tuple)) and
len(value) > 1 and
isinstance(value[-1], (Promise, str))
):
*value, label = value
value = tuple(value)
else:
label = key.replace('_', ' ').title()
labels.append(label)
# Use dict.__setitem__() to suppress defenses against double
# assignment in enum's classdict.
dict.__setitem__(classdict, key, value)
cls = super().__new__(metacls, classname, bases, classdict, **kwds)
cls._value2label_map_ = dict(zip(cls._value2member_map_, labels))
# Add a label property to instances of enum which uses the enum member
# that is passed in as "self" as the value to use when looking up the
# label in the choices.
cls.label = property(lambda self: cls._value2label_map_.get(self.value))
cls.do_not_call_in_templates = True
return enum.unique(cls)

....
--------------------------------------------------------------------
# type을 상속해서 만든 metaclass의 원형을 발견!
class EnumMeta(type):
"""
Metaclass for Enum
"""
@classmethod
def __prepare__(metacls, cls, bases, **kwds):
# check that previous enum members do not exist
metacls._check_for_existing_members(cls, bases)
# create the namespace dict
enum_dict = _EnumDict()
enum_dict._cls_name = cls
# inherit previous flags and _generate_next_value_ function
member_type, first_enum = metacls._get_mixins_(cls, bases)
if first_enum is not None:
enum_dict['_generate_next_value_'] = getattr(
first_enum, '_generate_next_value_', None,
)
return enum_dict

def __new__(metacls, cls, bases, classdict, **kwds):
# an Enum class is final once enumeration items have been defined; it
# cannot be mixed with other types (int, float, etc.) if it has an
# inherited __new__ unless a new __new__ is defined (or the resulting
# class will fail).
#
# remove any keys listed in _ignore_
classdict.setdefault('_ignore_', []).append('_ignore_')
ignore = classdict['_ignore_']
for key in ignore:
classdict.pop(key, None)
member_type, first_enum = metacls._get_mixins_(cls, bases)
__new__, save_new, use_args = metacls._find_new_(
classdict, member_type, first_enum,
)

# save enum items into separate mapping so they don't get baked into
# the new class
enum_members = {k: classdict[k] for k in classdict._member_names}
for name in classdict._member_names:
del classdict[name
....

--

--