본문은 참고한 블로그 및 자료를 제가 이해하기 쉽게 다시 정리한 글임을 밝힙니다.
1. 메타클래스(Metaclass)
파이썬에서는 모든 것이 객체이기 때문에 클래스도 그 자체로 객체이다. 그렇다면 클래스라는 객체를 생성하는 클래스는 무엇일까? 이처럼 “클래스의 클래스”를 메타클래스(metaclass)라고 한다.
파이썬에서 기본적으로 사용하는 메타클래스는 type
이다.
이
type
은 우리가 어떤 자료형의 타입을 알아내기 위해 사용하는type()
이 맞다!
1
2
3
4
5
class MyClass:
pass
type(MyClass)
# <class 'type'>
1-1. type
활용하기
이러한 메타클래스
type
을 활용하는 방법 두 가지를 살펴보자.
- 클래스 만들기
()
= 상속 클래스 정의를 담는 tuple{}
= 클래스에서 정의할 속성과 메서드를 담는 dict
1 2 3
MyClass = type('MyClass', (), {}) MyClass # <class '__main__.MyClass'>
커스텀 메타클래스 만들기
type
을 상속받아 커스텀 메타클래스를 정의하고, 이를 적용할 클래스의metaclass
파라미터로 지정하면 된다. 이러한 커스텀 메타클래스를 통해서 원하는 방법으로 클래스가 동작하도록 제어할 수 있다.예를 들어 클래스
MyClass
가 생성될 때 속성a
가 반드시 정수가 되어야 하는 상황을 가정해보자.이러한 검증 과정을
MyMetaClass
의__new__
메서드에 명시하면 된다.1 2 3 4 5 6 7
class MyMetaclass(type): def __new__(cls, clsname, bases, dct): assert type(dct['a']) is int, 'Attribute `a` is not an integer.' return type.__new__(cls, clsname, bases, dct) class MyClass(metaclass=MyMetaclass): a = 3.14
실행 결과, 다음과 같이 assert 문이 실행되는 것을 확인할 수 있다.
1 2 3 4 5 6 7
Traceback (most recent call last): File "/Users/.../test.py", line 7, in <module> class MyClass(metaclass=MyMetaclass): File "/Users/.../test.py",, line 4, in __new__ assert type(dct['a']) is int, 'Attribute `a` is not an integer.' ^^^^^^^^^^^^^^^^^^^^^ AssertionError: Attribute `a` is not an integer.
2. 메서드 관점에서 살펴본 인스턴스 생성 내부 동작
2-1. 클래스
메서드 이름 | 동작 |
---|---|
__new__ 메서드 | 클래스의 인스턴스를 생성한다. (메모리 할당) |
__init__ 메서드 | 생성된 인스턴스를 초기화한다. |
__call__ 메서드 | 인스턴스를 실행한다. |
__init__
메서드는 생성자가 아니라는 것에 주의해야 한다! 실제 생성은__new__
메서드에서 수행하며,__init__
메서드에서는 생성된 인스턴스를 초기화할 뿐이다.
2-2. 메타클래스
메타클래스 또한 __new__
, __init__
, __call__
메서드를 가지고 있다. 예제를 통해 동작 양상을 살펴보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class MyMetaClass(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 MyClass(metaclass=MyMetaClass):
def __new__(cls, *args, **kwargs):
print('child __new__')
obj = super().__new__(cls)
return obj
def __init__(self):
print('child __init__')
def __call__(self):
print('child __call__')
print('=============================================')
print('---- Instantiate MyClass:')
obj = MyClass()
print('---- Call MyClass Instance:')
obj()
print('=============================================')
print(f'{type(MyClass)=}')
1
2
3
4
5
6
7
8
9
10
11
metaclass __new__
metaclass __init__
=============================================
---- Instantiate MyClass:
metaclass __call__
child __new__
child __init__
---- Call MyClass Instance:
child __call__
=============================================
type(MyClass)=<class '__main__.MyMetaClass'>
위의 코드에서 알 수 있는 점은 다음과 같다.
MyClass
의 타입은MyMetaClass
이다.메타클래스
MyMetaClass
를 상속한 클래스MyClass
의 정의가 로드되는 순간, 이미MyMetaClass
는 생성 및 초기화 된다. 따라서MyMetaClass
의__new__
,__init__
메서드가 차례로 호출된다.MyClass
가 로드되는 순간에MyClass
라는 객체가 생성된 것이고, 객체가 생성되었다는 것은 클래스의 생성자가 호출되었다는 의미이다. 따라서MyClass
를 instantiate 하기 전에MyMetaClass
는 instantiate 된다.실제로 클래스
MyClass
를 instantiate 할 때에는MyMetaClass
인스턴스가 실행된다. 즉, 다음과 같이 실행되는 것이다.1
MyClass() == (MyMetaClass())()
따라서
MyMetaClass
의__call__
메서드가 호출된다. 그리고MyMetaClass
의__call__
메서드에서는 인스턴스의 생성자__new__
메서드를 호출하기 때문에,MyClass
의 인스턴스가 생성되는 것이다.
3. 활용 방안
앞서, 메타클래스를 통해서 원하는 방법으로 클래스가 동작하도록 제어할 수 있다고 했었다. 이때, 지금까지 살펴봤던 내용 중 떠올려야 할 메타클래스의 핵심 특징은 다음과 같다:
어떤 메타클래스를 상속받은 클래스를 instantiate 할 때마다, 메타클래스의
__call__
메서드가 호출된다.
따라서 메타클래스의 __call__
메서드에 어떤 클래스가 생성될 때마다 필요한 동작을 명시할 수 있는 것이다.
가장 대표적인 예시로 singleton 기능을 메타클래스를 통해 제공할 수 있다.
1
2
3
4
5
6
7
8
9
class SingletonType(type):
def __call__(cls, *args, **kwargs):
if not hasattr(cls, "_instance"):
cls._instance = super(SingletonType, cls).__call__(*args, **kwargs)
return cls._instance
class Foo(metaclass=SingletonType):
def __init__(self, name):
self.name = name
1
2
3
4
5
6
7
obj1 = Foo('name')
obj2 = Foo('name1')
print(obj1, obj2)
# <__main__.Foo object at 0x101646e90> <__main__.Foo object at 0x101646e90>
print(obj1 is obj2)
# True