메타 프로그래밍(metaprogramming)이란 컴퓨터 프로그램이 스스로를 조작하거나 자기 자신에 대한 정보를 다루는 프로그래밍 기법을 의미합니다. 파이썬은 메타클래스(metaclass)라는 특별한 형태의 클래스를 통해 클래스 수준의 메타 프로그래밍을 지원합니다.
파이썬에서 메타클래스를 활용한 메타 프로그래밍이란, 기존 코드를 수정하거나 감싸거나 새롭게 생성하는 방식으로 코드를 조작하는 함수와 클래스를 만드는 작업을 말합니다.
메타 프로그래밍의 핵심 요소
- 메타클래스(Metaclasses)
- 데코레이터(Decorators)
- 클래스 데코레이터(Class Decorators)
메타클래스란 무엇인가?
메타클래스를 한마디로 정의하면 "클래스를 생성하는 클래스"입니다. 파이썬은 모든 것이 객체인 객체지향 언어이며, 클래스 역시 예외가 아닙니다. class 키워드로 클래스를 정의하면 파이썬은 실제로 그 정의를 실행하여 하나의 객체를 생성합니다. 객체이기 때문에 변수에 할당하거나 복사하고, 속성을 추가하고, 함수의 인자로 전달하는 등 다양한 작업이 가능합니다.
메타클래스를 활용하면 클래스가 생성되는 과정 자체를 제어할 수 있습니다. 예를 들어 클래스의 속성을 수정하거나, 속성의 유효성을 검사하는 등의 작업을 수행할 수 있습니다.
파이썬의 내장 메타클래스: type
파이썬에서 모든 클래스의 메타클래스는 바로 type입니다.
class a:
pass
print(type(a))
실행 결과:
<class 'type'>
위 출력 결과에서 알 수 있듯이, 클래스 "a"는 type 클래스의 객체입니다. 즉, "a"는 type의 인스턴스라고 할 수 있으며, 이것이 곧 type이 메타클래스라는 의미입니다. 따라서 파이썬의 모든 클래스는 내장 메타클래스인 type에 속합니다.
type으로 클래스 동적으로 생성하기
앞서 살펴본 것처럼 type은 기본 메타클래스입니다. 이 기본 메타클래스를 이용해 새로운 클래스를 직접 만들 수도 있습니다.
>>> class hello(object):
... pass
위 코드는 type()을 통해 다음과 같이 직접 생성할 수도 있습니다.
>>> hello = type('hello', (), {})
>>> print(hello) # 클래스 객체 반환
>>> print(hello()) # 클래스로 인스턴스 생성
<__main__.hello object at 0x05D86830>
type은 클래스의 속성을 정의하기 위해 딕셔너리를 인자로 받습니다. 따라서 아래 코드는,
>>> class hello(object):
... world = True
다음 코드와 완전히 동일합니다.
hello = type('hello', (), {'world': True})
커스텀 메타클래스 만들기
그렇다면 언제 커스텀 메타클래스를 만들어야 할까요? 바로 클래스 생성 과정 자체를 제어하고 싶을 때, 예를 들어 클래스의 속성을 검증하거나 수정하고 싶을 때입니다.
클래스의 인스턴스화 과정은 다음 두 단계로 이루어집니다.
__new__()를 호출하여 인스턴스를 생성__init__()를 호출하여 생성된 인스턴스를 초기화
따라서 커스텀 메타클래스를 만든다는 것은 사실상 부모 클래스의 __new__() 또는 __init__ 메서드를 재정의하는 것입니다.
예제 1 — 클래스 속성 이름을 소문자로 변환하는 메타클래스
class LowercaseMeta(type):
def __new__(mcs, name, bases, attrs):
lower_attrs = {}
for k, v in attrs.items():
if not k.startswith('__'):
lower_attrs[k.lower()] = v
else:
lower_attrs[k] = v
return type.__new__(mcs, name, bases, lower_attrs)
class LowercaseClass(metaclass=LowercaseMeta):
BAR = True
def HELLO(self):
print('hello')
print(dir(LowercaseClass))
LowercaseClass().hello()
실행 결과
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bar', 'hello']
hello
출력 결과를 보면 BAR는 bar로, HELLO는 hello로 자동 변환된 것을 확인할 수 있습니다. 단, __로 시작하는 던더(dunder) 메서드들은 원형 그대로 유지됩니다.
예제 2 — 클래스에 새로운 메서드 추가하기
class ListMeta(type):
def __new__(mcs, name, bases, attrs):
attrs['add'] = lambda self, value: self.append(value)
return type.__new__(mcs, name, bases, attrs)
class MyList(list, metaclass=ListMeta):
pass
l = MyList()
l.add(1)
print(l)
def class_decorator(cls):
cls.add = lambda self, value: self.append(value)
return cls
@class_decorator
class MyList(list):
pass
l = MyList()
l.add(1)
print(l)
실행 결과
[1]
[1]
이 예제는 메타클래스와 클래스 데코레이터가 비슷한 목적(클래스에 메서드 동적 추가)으로 사용될 수 있음을 보여줍니다. 일반적으로 단순한 기능 추가에는 데코레이터가 더 간결하고, 클래스 생성 과정 전체를 제어해야 할 때 메타클래스가 적합합니다.
__metaclass__ 속성 이해하기
클래스 자신 또는 그 베이스(base) 클래스 중 하나에 __metaclass__ 속성이 정의되어 있으면 해당 속성이 메타클래스로 사용되고, 정의되어 있지 않으면 type이 메타클래스로 사용됩니다.
예를 들어 다음과 같이 클래스를 정의했다고 가정해 보겠습니다.
class hello(object):
x = 10
위의 hello 클래스에는 __metaclass__ 속성이 없으므로 대신 type이 사용되며, 클래스 생성은 다음과 같이 이루어집니다.
hello = type(name, bases, dict)
반면 hello 클래스에 메타클래스가 정의되어 있다면 어떻게 될까요?
class hello(object):
__metaclass__ = myMetaClass
pass
이 경우에는 클래스 생성 시 myMetaClass가 type 대신 사용됩니다.
hello = myMetaClass(name, bases, dict)
참고: 위의 __metaclass__ 속성 방식은 파이썬 2에서 사용되던 문법입니다. 파이썬 3부터는 앞선 예제 1, 2에서 본 것처럼 class 키워드의 metaclass 키워드 인자(class Foo(metaclass=MyMeta):)를 사용해야 하므로 참고하시기 바랍니다.