python class를 생성할 때 __new__, __init__, __call__ 를 활용하는 방법에 대해서 먼저 알아본 후, 메타클래스에 생성자를 활용하여 클래스를 커스터마이징 하는 방법에 대하여 알아보았다.

파이썬의 생성자 2가지

python에서 객체를 생성할 때는 __new__와 __init__ 가 사용된다.

  • __new__ 는 생성자이다. 클래스의 인스턴스를 만들기만 한다.
  • __init__ 는 클래스의 속성을 초기화한다.(initialize)

new 를 실행할 때 주의점

  • 클래스 자기 자신을 인자로 받는다. 주로 cls 이라고 씀.
    • def __new__(cls, *args, **kwargs)
    • 보통 instance(주로 self 라고 쓰는 그것)를 인자로 받는 것과 차이가 있다.
  • return이 반드시 인스턴스가 아니어도 된다.
    • 단, 인스턴스를 반환하지 않을 경우는 인스턴스화가 되지 않음에 유의.

init 를 실행하는 2가지 방법

  • 클래스에서 실행하는 방법
    • cls.__init__(instance, *args, **kwargs)
    • 클래스에서 실행할 때에는 instance를 지정해줘야한다. (보통 self로 쓰는 그것)
  • 인스턴스에서 실행하는 방법
    • instance.__init__(*args, **kwargs) (instance는 인스턴스 객체)
    • 인스턴스에서 실행할 때에는 self에 인스턴스 자기자신이 자동으로 들어간다.

__call__의 역할

  • __new__ 가 인스턴스 객체를 반환하지 않을 경우, __call__은 __init__를 실행하지 않는다.
  • __call__ 을 오버라이딩할 경우, __new__와 __init__를 따로 실행해줘야 한다.
    • __new__는 __init__를 자동으로 실행하지 않음
    • __new__(cls, [...]) , __init__(instance, [...]) ⇒ __new__와 __init__는 첫번째 인자만 다르고 나머지는 동일하게 입력한다.

메타 클래스에서 생성자 자유로이 사용하기

※ 클래스에서 생성자를 사용하는 것과 헷갈릴 수 있으므로 주의하기.

  • 위 설명에서 cls는 meta, self는 cls로 바꿔서 이해하면 된다. (메타클래스는 클래스의 클래스이므로)

파이썬의 최상위 메타클래스인 type을 상속 받아서 __new__, __init__, __call__ 를 오버라이딩한다.

class MetaType(type): # type을 상속 받아서 metaclass를 생성
    def __new__(meta, metaname, bases, attrs): # cls가 아니라 meta를 받음
        print('MetaType is used')
        return super(MetaType, meta).__new__(meta, metaname, bases, attrs)
#         return type.__new__(meta, metaname, bases, attrs)   # 이렇게도 가능

    def __init__(cls, *args, **kwargs):
        print("MyClass is initialized")
        cls.a = 'class attribute from meta'  # 메타클래스에서 클래스의 속성을 정의 가능
      
    def __call__(cls, *args, **kwargs):
        print('MyClass is called')
        instance = cls.__new__(cls, *args, **kwargs)    # 인스턴스만 생성
#         instance.__init__(*args, **kwargs)   # 이렇게도 가능
        cls.__init__(instance, *args, **kwargs)         # 인스턴스 속성 초기화
        return instance # class에 call 했을때 인스턴스 객체를 반환

메타클래스를 지정해서 클래스를 생성한다. 클래스는 메타클래스의 인스턴스이므로 이는 곧 메타클래스를 인스턴스화하는 것과 같다. 따라서 메타클래스 MetaType의 __new__ 와 __init__가 실행되고 프린트문이 출력된다.

class MyClass(metaclass=MetaType):
    b = 'class attribute'
    
    def __init__(self, d):
        self.c = 'instance attribute'
        self.d = d

# print
# MetaType is used
# MyClass is initialized

a 속성은 MetaType의 __init__에서 정의하였다. 메타클래스의 __init__에서 속성을 정의하면 클래스의 속성이 초기화된다.(클래스에서 __init__를 정의하면 인스턴스의 속성이 초기화되는 것과 같다.) b 속성은 MyClass를 선언하면서 직접 정의하였다.

print(MyClass.a)
print(MyClass.b)

# print
# class attribute from meta
# class attribute

MyClass를 인스턴스화한 myInstance를 생성하였다. 메타클래스에서 정의한 __call__은 클래스를 호출할 때의 동작이다. 따라서 MetaType에서 정의한 __call__ 안에 있는 print문이 실행된다.

myInstance = MyClass(1)

# print
# MyClass is called

MyClass의 __init__에서 정의한 c 속성과, 클래스를 인스턴스할때 인자로 사용한 d 속성 모두 myInstance의 속성으로 포함되어 있다.

vars(myInstance)

# output
# {'c': 'instance attribute', 'd': 1}