37. 메타클래스(Metaclass)
1. 키워드
- 메타클래스(Metaclass)
- 싱글톤(Singleton)
2. 메타클래스 사용하기
- 메타클래스는 클래스를 만드는 클래스인데, 이 메타클래스를 구현하는 방법은 다음과 같이 두 가지가 있다.
1] type
을 사용하여 동적으로 클래스를 생성하는 방식
2] type
을 상속받아서 메타클래스를 구현하는 방식
type
은 객체의 클래스(자료형) 종류를 알아낼 때도 사용할 수 있고, 클래스를 만들어낼 수도 있다.
1) 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 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):
과 같이metaclass
에Singleton
을 지정한다. - 이렇게 하면 메타클래스
Singleton
이 클래스Hello
의 동작을 제어할 수 있다. - 보통
__call__
메서드는 인스턴스를()
(괄호)로 호출할 때 호출된다. - 하지만
type
을 상속받은 메타클래스에서__call__
메서드를 구현하면 메타클래스를 사용하는 클래스로 인스턴스를 만들 때__call__
메서드가 호출된다. - 여기서는
Hello()
로 인스턴스를 만들 때Singleton
의__call__
메서드가 호출된다. - 따라서
__call__
안에서 이미 인스턴스가 생성되지 않았는지(중복되는지) 확인하고, 생성되지 않았으면 인스턴스를 생성하여 속성에 저장한 뒤 반환한다. - 만약 인스턴스가 생성되어 있다면(중복된다면) 인스턴스를 생성하지 않고 바로 반환한다.
- 대표적인 메타클래스가
abc.ABCMeta
인데,abc.ABCMeta
를 사용한 추상 클래스는 메서드 목록만 가지도록 만들고, 파생 클래스에서 메서드 구현을 강제한다. - 즉, 메타클래스로 클래스의 동작을 제어한다.