매직 메서드 __init_subclass__ 의 동작


클래스 변수가 어디서 생기는 거지…? 🤔


회사 레거시 코드에서 __init_subclass__ 를 처음 봤을 땐, init이 들어있는 것으로 보아 인스턴스가 생성될 때 같이 생성되는 프로퍼티일 것이라 짐작했다. 그러던 어느날 다른 팀원 분의 질문으로 해당 magic method를 정리하기까지 이르렀다.

문제가 된 부분은 클래스 변수가 빈 dict를 만드는데, 로직을 보면 해당 dict에서 여러 값들을 꺼내주고 있었고 그 어떤 로직에서도 해당 dict를 채워주는 곳이 없던 것이다. 일반적으로 클래스의 속성이나 변수는 인스턴스를 생성했을 때에 생성된다고 생각하기 때문에, 분명 로직 어딘가에 있을 것이라고 생각했다. 그런데 해당 변수를 아무리 검색해도 찾을 수 없다는 것을 알게됐고, 해당 class를 정의할 때 이미 클래스 변수가 채워진다는 결론에 이르렀다.

결론적으로 __init_subclass__ 메서드는 부모 클래스를 상속받은 자식 클래스를 정의했을 때 호출된다는 것을 알게되었다.

# 예시
class Parent:
    def __init_subclass__(cls):
        print('Subclass of Parent Created!')

class Child(Parent):
    pass

# 결과: 상속 받은 클래스는 정의만으로 init_subclass method가 호출됨. 'Subclass of Parent Created!' 가 print 되는 것

만약 자식 클래스에서 __init_subclass__가 재정의될 경우, 자식 클래스의 subclass hierarchy rule이 적용된다. child -> parent 순으로 찾기 때문에, child에서 해당 메서드가 정의되어있을 경우 child의 내용으로 적용된다.

# 예시
class Parent:
    def __init_subclass__(cls):
        print('AAA')

class Child(Parent):
    def __init_subclass__(cls):
        print('BBB')

# 결과: AAA가 print됨

class Grandchild(Child):
    pass

# 결과: BBB가 print됨

언제 사용?

버전 3.6에서 메타 클래스의 대안으로 소개된 것이라고 한다. PEP 487에는 서브 클래스를 등록하거나, 서브 클래스들의 default attribute를 세팅할 때 유용하다고 언급되어 있다. 실제 내부 코드에서도 부모 클래스를 상속하는 여러 자식 클래스가 있고, 해당 클래스가 공유하는 클래스 변수가 있을 경우에 사용되고 있다.

강의에서 나온 예시는 아래와 같다.


class PEP487:
    def __init_subclass_(cls, whom, **kwargs):
      super().__init_subclass__(**kwargs)
      cls.hello = lambda: print(f"Hello, {whom}")

class HelloWorld(PEP487, whom="World")
    pass

>>> HelloWorld.hello()

# Hello, World

메타 클래스

클래스를 만들어주는 클래스다. type을 사용해 동적으로 생성하거나, type을 상속받아 구현한다.

# 동적 구현의 예
Parent = type('Parent', (), {'is_parent':True})
Child = type('Child', (Parent,), {'is_parent':False})

>>> Parent.is_parent # True
>>> Child.is_parent # False