48.7 메타클래스 사용하기

메타클래스(metaclass)는 클래스를 만드는 클래스입니다. 메타클래스는 type을 사용하여 동적으로 클래스를 생성하는 방식과 type을 상속받아서 메타클래스를 구현하는 방식이 있습니다.

먼저 클래스는 type 안에 클래스 이름(문자열), 기반 클래스 튜플, 속성과 메서드 딕셔너리를 지정해서 만듭니다. type은 객체의 클래스(자료형) 종류를 알아낼 때도 사용할 수 있고, 클래스를 만들어낼 수도 있습니다.

  • 클래스 = type('클래스이름', 기반클래스튜플, 속성메서드딕셔너리)
>>> Hello = type('Hello', (), {})    # type으로 클래스 Hello 생성
>>> Hello
<class '__main__.Hello'>
>>> h = Hello()                      # 클래스 Hello로 인스턴스 h 생성
>>> h
<__main__.Hello object at 0x029B4750>

type('Hello', (), {})는 클래스 이름이 Hello고 기반 클래스와 속성, 메서드가 없는 클래스를 만듭니다. 이렇게 만든 클래스는 인스턴스를 생성할 수 있습니다.

그럼 클래스에 속성과 메서드를 넣고 상속도 사용해보겠습니다. 다음은 list를 상속받고, 속성 desc와 메서드 replace가 들어있는 클래스를 만듭니다.

metaclass.py

def replace(self, old, new):    # 클래스에 들어갈 메서드 정의
    while old in self:
        self[self.index(old)] = new
 
# list를 상속받음, 속성 desc, 메서드 replace 추가
AdvancedList = type('AdvancedList', (list, ), { 'desc': '향상된 리스트', 'replace': replace })
 
x = AdvancedList([1, 2, 3, 1, 2, 3, 1, 2, 3])
x.replace(1, 100)
print(x)         # [100, 2, 3, 100, 2, 3, 100, 2, 3]
print(x.desc)    # 향상된 리스트

이렇게 type의 기반 클래스 튜플에 기반 클래스를 지정하고, 속성과 메서드 딕셔너리에 속성과 메서드를 넣으면 됩니다. 이때 기반 클래스가 한 개이면 (list, )와 같이 튜플로 만들어줍니다. 그리고 속성과 메서드를 딕셔너리에 넣을 때 이름은 반드시 문자열 형태로 지정해줍니다. 그리고 메서드가 간단하다면 def로 함수를 작성하지 않고 람다 표현식을 사용해도 됩니다.

이번에는 메타클래스의 __new__ 메서드를 알아보겠습니다. 클래스가 type을 상속받으면 메타클래스가 됩니다. 이때 __new__ 메서드에서 새로 만들어질 클래스에 속성과 메서드를 추가해줄 수 있습니다.

class 메타클래스이름(type):
    def __new__(metacls, name, bases, namespace):
        코드

type_inheritance_metaclass.py

class MakeCalc(type):    # type을 상속받음
    def __new__(metacls, name, bases, namespace):      # 새 클래스를 만들 때 호출되는 메서드
        namespace['desc'] = '계산 클래스'              # 새 클래스에 속성 추가
        namespace['add'] = lambda self, a, b: a + b    # 새 클래스에 메서드 추가
        return type.__new__(metacls, name, bases, namespace)    # type의 __new__ 호출
 
Calc = MakeCalc('Calc', (), {})    # 메타클래스 MakeCalc로 클래스 Calc 생성
c = Calc()                         # 클래스 Calc로 인스턴스 c 생성
print(c.desc)                      # '계산 클래스': 인스턴스 c의 속성 출력
print(c.add(1, 2))                 # 3: 인스턴스 c의 메서드 호출

__new__ 메서드는 Calc = MakeCalc('Calc', (), {})처럼 메타클래스로 새 클래스를 만들 때 호출됩니다. 따라서 이 메서드 안에서 새 클래스에 속성과 메서드를 추가해줄 수 있습니다. 여기서는 lambda self, a, b: a + b와 같이 간단하게 람다 표현식으로 메서드를 추가했습니다. 특히 메서드의 첫 번째 매개변수는 반드시 self라야 하므로 람다 표현식에서도 self를 지정해주어야 합니다.

보통 메타클래스는 주로 클래스의 동작을 제어할 때 사용합니다. 다음은 싱클톤(Singleton)이라는 방식인데 클래스의 인스턴스를 언제나 하나만 생성해냅니다.

class Singleton(type):    # type을 상속받음
    __instances = {}      # 클래스의 인스턴스를 저장할 속성
    def __call__(cls, *args, **kwargs):    # 클래스로 인스턴스를 만들 때 호출되는 메서드
        if cls not in cls.__instances:     # 클래스로 인스턴스를 생성하지 않았는지 확인
            cls.__instances[cls] = super().__call__(*args, **kwargs)
                                           # 생성하지 않았으면 인스턴스를 생성하여 속성에 저장
        return cls.__instances[cls]        # 클래스로 인스턴스를 생성했으면 인스턴스 반환
 
class Hello(metaclass=Singleton):    # 메타클래스로 Singleton을 지정
    pass
 
a = Hello()     # 클래스 Hello로 인스턴스 a 생성
b = Hello()     # 클래스 Hello로 인스턴스 b 생성
print(a is b)   # True: 인스턴스 a와 b는 같음

먼저 type을 상속받은 메타클래스 Singleton을 만들고, 클래스 Hello를 만들 때 class Hello(metaclass=Singleton):와 같이 metaclassSingleton을 지정합니다. 이렇게 하면 메타클래스 Singleton이 클래스 Hello의 동작을 제어할 수 있습니다.

보통 __call__ 메서드는 인스턴스를 ( )(괄호)로 호출할 때 호출됩니다. 하지만 type을 상속받은 메타클래스에서 __call__ 메서드를 구현하면 메타클래스를 사용하는 클래스로 인스턴스를 만들 때 __call__ 메서드가 호출됩니다(클래스를 ( )로 호출할 때).

여기서는 Hello()로 인스턴스를 만들 때 Singleton__call__ 메서드가 호출됩니다. 따라서 __call__ 안에서 이미 인스턴스가 생성되지 않았는지(중복되는지) 확인하고, 생성되지 않았으면 인스턴스를 생성하여 속성에 저장한 뒤 반환합니다. 만약 인스턴스가 생성되어 있다면(중복된다면) 인스턴스를 생성하지 않고 바로 반환합니다.

대표적인 메타클래스가 '38.6 추상 클래스 사용하기'에서 설명한 abc.ABCMeta입니다. abc.ABCMeta를 사용한 추상 클래스는 메서드 목록만 가지도록 만들고, 파생 클래스에서 메서드 구현을 강제합니다. 즉, 메타클래스로 클래스의 동작을 제어합니다.