__init_subclass__ 훅은 Python 3.6에 PEP 487의 일환으로 도입되었습니다. 그 이전에는 서브클래싱을 수행하려는 모든 클래스—예: 등록, 검증 또는 자동 필드 수집 등—은 사용자 정의 메타클래스를 선언해야 했습니다. 메타클래스는 강력하지만, 적절히 조정되지 않으면 다중 상속 시 마찰을 일으킵니다. 새로운 훅은 기본 클래스가 특정 메타클래스를 채택하도록 강제하지 않고도 서브클래스 초기화에 참여할 수 있도록 하여, 이전에 복잡한 메타클래스 조작에 의존했던 Django ORM 및 SQLAlchemy와 같은 프레임워크를 단순화합니다.
클래스 B가 기본 클래스 A로부터 상속할 때, 프레임워크 개발자는 종종 클래스 B가 정의되는 순간에 논리를 실행해야 할 필요가 있습니다—인스턴스가 생성되기 전에. 예를 들어, ORM은 B로부터 모든 열 정의를 수집하고 이를 레지스트리에 저장해야 할 필요가 있습니다. 메타클래스를 사용하려면 A가 type 또는 사용자 정의 메타클래스를 메타클래스로 가져야 하며, 이는 B가 또 다른 메타클래스(예: ABC 또는 다른 프레임워크)도 사용해야 할 경우 문제가 됩니다. 이로 인해 해결하기 어려운 메타클래스 충돌 오류가 발생합니다. 또한, 메타클래스 __new__는 클래스 네임스페이스가 완전히 채워지기 전에 실행되므로 최종 클래스 속성을 검사하기 어렵습니다.
Python은 __init_subclass__ 클래스를 제공합니다. 클래스가 이 메소드를 정의하면, 정의된 클래스가 직접 부모로 있을 때마다 자동으로 호출됩니다. 훅은 새로 생성된 서브클래스를 첫 번째 인수로 받고, 클래스 정의 줄에서 전달된 키워드 인수(예: class B(A, keyword=value))를 받습니다.
class RegistryBase: _registry = {} def __init_subclass__(cls, category="default", **kwargs): super().__init_subclass__(**kwargs) print(f"Registering {cls.__name__} under category '{category}'") cls._registry[cls.__name__] = {"class": cls, "category": category} class Plugin(RegistryBase, category="audio"): pass class Effect(Plugin, category="reverb"): pass
메타클래스 __new__와 달리, 이는 클래스 객체가 완전히 구성된 후에 실행됩니다. 이로 인해 훅은 cls.__dict__, 메소드 및 주석을 안전하게 검사할 수 있습니다. 또한, 이 훅은 MRO를 존중하며, super()가 호출될 때 부모 클래스 등록이 자식 클래스 로직보다 먼저 실행되도록 보장합니다.
대규모 오디오 처리 SaaS 플랫폼에서 엔지니어링 팀은 서드파티 개발자가 기본 AudioEffect 클래스를 서브클래싱하여 오디오 효과를 정의할 수 있는 플러그인 시스템을 구현해야 했습니다. 각 서브클래스는 effect_name, latency_ms, category와 같은 메타데이터를 포함하는 글로벌 효과 카탈로그에 자동으로 등록되어야 했습니다. 문제는 플랫폼이 이미 데이터베이스 모델용으로 SQLAlchemy 선언적 기본값(메타클래스를 사용)을 사용하고 있었고, 일부 오디오 효과가 AudioEffect와 SQLAlchemy 모델 모두에서 상속받아야 했습니다. AudioEffect에 사용자 정의 메타클래스를 도입하면 SQLAlchemy의 DeclarativeMeta와 메타클래스 충돌이 발생하여 애플리케이션이 시작되지 않았습니다.
첫 번째 접근법은 데코레이터를 사용한 수동 등록이었습니다. 개발자들은 각 클래스 정의 위에 @register_effect를 작성해야 했습니다. 이는 작동했지만 오류가 발생하기 쉬웠습니다; 개발자들은 종종 데코레이터를 잊어버려 프로덕션에서 효과가 누락되곤 했습니다. 또한, 데코레이터 인수와 클래스 정의에서 메타데이터를 반복해야 했으므로 DRY 원칙에 위배되었습니다.
두 번째 접근법은 DeclarativeMeta와 EffectMeta에서 상속을 곱하는 공통 메타클래스를 사용하는 것이었습니다. 이는 즉각적인 충돌을 해결했지만 취약한 의존성을 야기했습니다. SQLAlchemy가 내부 메타클래스 논리를 업데이트할 때마다 플랫폼이 파손되었습니다. 또한 모든 효과 클래스가 데이터베이스 모델이 되어야 했으며, 이는 경량 클라이언트 측 효과에는 적절하지 않았습니다.
세 번째 접근법은 __init_subclass__를 활용했습니다. AudioEffect 기본 클래스는 클래스 정의 중에 전달된 키워드 인수를 캡처하기 위해 __init_subclass__를 정의했습니다. 예를 들어 개발자가 class Reverb(AudioEffect, effect_id="rvb-01", version=2)와 같이 작성하면, 훅은 자동으로 ID의 고유성을 검증하고 클래스는 스레드 안전한 WeakValueDictionary 레지스트리에 등록되었습니다. 이는 메타클래스 충돌을 완전히 피할 수 있었습니다. 왜냐하면 __init_subclass__는 어떤 메타클래스와도 협력하는 일반 클래스 메소드이기 때문입니다.
팀은 세 번째 솔루션을 선택했습니다. 이는 SQLAlchemy와의 호환성을 유지하며, 데코레이터의 필요를 없애고, 등록이 임포트 시 자동으로 발생하도록 보장했습니다. 결과적으로 시스템은 "그냥 작동"하는 플러그인 시스템을 구현했습니다—개발자들은 서브클래싱하고 매개변수를 인라인으로 선언하기만 하면 되었습니다. 이 시스템은 단 한 번의 메타클래스 충돌 없이 150개 이상의 효과를 성공적으로 등록하였으며, 메타클래스 접근법에 비해 MRO 계산 복잡성이 줄어들어 시작 시간이 40% 개선되었습니다.
부모가 그것을 정의하지 않더라도 __init_subclass__는 항상 super().__init_subclass__()를 호출해야 하는 이유는 무엇인가요?
후보자들은 종종 object가 __init_subclass__를 정의하지 않기 때문에 호출이 선택 사항이라고 가정합니다. 그러나 다중 상속 시나리오에서 super()를 호출하지 않으면 훅을 구현한 형제 클래스들의 체인이 끊어질 수 있습니다. Python의 협력적 다중 상속에서는 모든 참가자가 다이아몬드 패턴을 따라 super()를 호출하여 계층의 모든 가지가 초기화 논리를 실행하도록 해야 합니다. 만약 A와 B 모두 __init_subclass__를 정의하고 있고, C(A, B)가 A의 훅만 호출하면 B의 등록 로직은 조용히 건너뛰어져 플러그인 시스템에서 미묘한 버그가 발생합니다.
__init_subclass__는 메소드의 네임스페이스를 수정하거나 동적으로 메소드를 추가할 수 있으며, __slots__에 대한 의미는 무엇인가요?
후보자들은 종종 __init_subclass__를 메타클래스 __new__와 혼동합니다. __init_subclass__는 클래스가 완전히 생성된 후에 실행되기 때문에 생성 전에 클래스 딕셔너리를 수정할 수 없습니다(메타클래스의 __prepare__ 또는 __new__와 달리). 그러나 setattr(cls, name, value)를 사용하여 동적으로 속성을 추가할 수 있습니다. 위험은 __slots__에 발생합니다: 부모 클래스가 __slots__를 사용하면 서브클래스가 그 제약 조건을 상속합니다. __init_subclass__에서 setattr을 사용하여 슬롯 클래스에 새 속성을 추가하려고 하면, 서브클래스가 스스로 __slots__ 또는 __dict__를 정의하지 않는 한 AttributeError가 발생합니다. 이 제한으로 인해 설계자들은 등록/메타데이터를 위해 __init_subclass__를 사용할 것인지, 클래스 본체의 구조적 수정을 위해 메타클래스를 사용할 것인지 선택해야 합니다.