Skip to content

37. 메타클래스(Metaclass)


1. 키워드

  • 메타클래스(Metaclass)
  • 싱글톤(Singleton)


2. 메타클래스 사용하기

  • 메타클래스는 클래스를 만드는 클래스인데, 이 메타클래스를 구현하는 방법은 다음과 같이 두 가지가 있다.


1] type을 사용하여 동적으로 클래스를 생성하는 방식

2] type을 상속받아서 메타클래스를 구현하는 방식


  • type은 객체의 클래스(자료형) 종류를 알아낼 때도 사용할 수 있고, 클래스를 만들어낼 수도 있다.


1) type을 사용하여 동적으로 클래스 생성하기

  • 먼저 클래스는 type 안에 클래스 이름(문자열), 기반 클래스 튜플, 속성과 메서드 딕셔너리를 지정해서 만든다.


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

>>> h = Hello() # 클래스 Hello로 인스턴스 h 생성
>>> h
<__main__.Hello object at ...>


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


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


def replace(self, old, new): # 클래스에 들어갈 메서드 정의
    while old in self:
        self[self.iindex(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로 함수를 작성하지 않고 람다 표현식을 사용해도 된다.


2) type을 상속받아서 메타클래스 구현하기

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


class 메타클래스이름(type):
    def __new__(metacls, name, bases, namespace):
        코드
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를 지정해 주어야 한다.


3) 메타클래스 활용하기

  • 메타클래스는 주로 클래스의 동작을 제어할 때 사용한다.
  • 다음은 싱글톤이라는 방식인데 클래스의 인스턴스를 언제나 하나만 생성해 낸다.


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=Singletone):과 같이 metaclassSingleton을 지정한다.
  • 이렇게 하면 메타클래스 Singleton이 클래스 Hello의 동작을 제어할 수 있다.
  • 보통 __call__ 메서드는 인스턴스를 ()(괄호)로 호출할 때 호출된다.
  • 하지만 type을 상속받은 메타클래스에서 __call__ 메서드를 구현하면 메타클래스를 사용하는 클래스로 인스턴스를 만들 때 __call__ 메서드가 호출된다.
  • 여기서는 Hello()로 인스턴스를 만들 때 Singleton__call__ 메서드가 호출된다.
  • 따라서 __call__ 안에서 이미 인스턴스가 생성되지 않았는지(중복되는지) 확인하고, 생성되지 않았으면 인스턴스를 생성하여 속성에 저장한 뒤 반환한다.
  • 만약 인스턴스가 생성되어 있다면(중복된다면) 인스턴스를 생성하지 않고 바로 반환한다.
  • 대표적인 메타클래스가 abc.ABCMeta인데, abc.ABCMeta를 사용한 추상 클래스는 메서드 목록만 가지도록 만들고, 파생 클래스에서 메서드 구현을 강제한다.
  • 즉, 메타클래스로 클래스의 동작을 제어한다.

References