Python프로그래밍시니어 파이썬 개발자

파이썬 인스턴스가 메서드 해석 순서 계산에 참여하기 위해 논리적 기본 클래스를 지정하는 수단은 무엇인가요?

Hintsage AI 어시스턴트로 면접 통과

질문에 대한 답변

질문의 역사

__mro_entries__ 프로토콜은 Python 3.7에서 PEP 560를 통해 도입되었습니다 ("타이핑 모듈과 제네릭 타입에 대한 기본 지원"). 이 개선 이전에는 typing.List[int]와 같은 제네릭 별칭을 클래스 정의에서 기본 클래스로 사용할 수 없었습니다. 왜냐하면 type.__new__가 모든 기본이 type의 인스턴스여야 한다고 엄격히 요구했기 때문입니다. 이 제한으로 인해 typing 모듈은 유지 관리가 어려운 부서진 메타클래스 해킹에 의존해야 했고 성능 문제를 야기했습니다. 이 프로토콜은 기본 형식의 구문 표현을 상속 그래프에 대한 의미론적 기여로부터 분리하도록 설계되어 제네릭 및 팩토리 패턴에 대한 깨끗한 지원을 가능하게 합니다.

문제

CPython이 클래스 정의를 처리할 때 메서드 해석 순서(MRO)를 계산해야 하며, 이를 위해 C3 선형화 알고리즘을 사용하여 일관되고 예측 가능한 메서드 조회 계층을 보장해야 합니다. 만약 기본 객체가 클래스가 아니라면 (예: 매개변수화된 제네릭 또는 구성 객체), 인터프리터는 새로운 클래스를 상속 트리 내에서 올바르게 배치할 수 있는 필요한 타입 정보를 갖고 있지 않습니다. 그러한 객체를 단순히 무시하면 isinstance 확인 및 super() 체인이 깨질 것이며, 이를 전면 거부하면 강력한 메타 프로그래밍 패턴을 방해할 것입니다. 핵심 도전 과제는 이러한 비클래스 객체가 클래스 생성 단계에서 어떤 구체적인 클래스를 논리적으로 나타내는지를 선언하도록 허용하는 것이었습니다.

해결책

Python은 이제 클래스 생성 중에 기본 튜플의 각 항목을 검사하며, __mro_entries__(self, bases) 메서드가 있는지를 확인합니다. 이 메서드가 존재한다면, 원래의 기본 튜플을 인자로 호출하며, MRO 계산에서 객체를 대체할 실제 클래스의 튜플을 반환해야 합니다. 반환된 클래스는 마치 명시적으로 기본으로 목록화된 것처럼 취급됩니다. 이 메커니즘은 인스턴스가 정의 시점에 구체적인 클래스로 해석되는 투명한 자리 표시자 역할을 할 수 있게 합니다.

class ConfigurableMixin: def __init__(self, feature): self.feature = feature def __mro_entries__(self, bases): # 구성에 따라 기본 클래스를 동적으로 주입합니다. if self.feature == "logging": return (LoggingSupport,) return (BaseFeature,) class LoggingSupport: def log(self, msg): print(msg) class BaseFeature: pass # 인스턴스는 MRO에서 LoggingSupport로 대체됩니다. class Service(ConfigurableMixin("logging")): pass print(LoggingSupport in Service.__mro__) # True

실제 사례

대규모 비동기 웹 프레임워크에서 개발자들은 특정 데이터베이스 URL(예: DatabaseMixin("postgresql://"))로 인스턴스화할 때, 사용자의 서비스 클래스에 자동으로 ConnectionPoolAsyncSession을 기본 클래스로 주입하는 DatabaseMixin 팩토리를 생성해야 했습니다. 어려운 점은 DatabaseMixin(...)이 클래스를 반환하는 것이 아니라 평범한 객체 인스턴스를 반환했지만, 개발자가 class UserService(ConnectionPool, AsyncSession)라고 명시적으로 작성한 것처럼 MRO에 참여해야 했다는 점입니다.

해결책 1: 사용자 정의 메타클래스 하나의 접근 방식은 __new__에서 bases 튜플을 스캔하고 DatabaseMixin 인스턴스를 확인하여 해당 클래스를 목표 클래스로 대체한 뒤 super().__new__를 호출하는 메타클래스를 만드는 것이었습니다. 이는 정밀한 제어를 가능하게 했지만, "메타클래스 충돌" 문제를 초래했습니다: 이 메타클래스를 사용하는 어떤 서비스도 자신의 메타클래스를 정의한 다른 클래스(예: 특정 ORM 기본 클래스)로부터 상속할 수 없습니다. 또한 클래스 정의 구문이 복잡한 변환을 숨기기 때문에 디버깅이 어려워졌고, 스택 추적은 사용자 코드 대신 메타클래스 내부를 가리켰습니다.

해결책 2: 생성 후 클래스 장식 또 다른 옵션은 클래스가 생성된 후에 적용되는 클래스 장식자를 사용하는 것이었습니다. 장식자는 ConnectionPoolAsyncSession에서 메서드를 수동으로 새로운 클래스에 복사하거나 type.__setattr__를 사용하여 주입했습니다. 이는 메타클래스 전염성을 피했지만, 본질적으로 Python의 상속 모델을 깨뜨렸습니다: isinstance(UserService(), ConnectionPool)False를 반환하며, 복사된 메서드 내의 super() 호출은 잘못된 상위 클래스를 해결하게 됩니다. 이는 프레임워크 유틸리티가 서비스를 데이터베이스 기능으로 인식하지 못하게 만드는 미세한 버그로 이어졌습니다.

해결책 3: __mro_entries__ 프로토콜 팀은 DatabaseMixin에서 반환한 객체에 __mro_entries__를 구현하기로 결정했습니다. 이 메서드는 구문 분석된 URL에 따라 (ConnectionPool, AsyncSession)을 반환했습니다. 이 솔루션은 CPython의 기본 클래스 생성 기계와 원활하게 통합되었습니다. MRO는 올바르게 계산되었고, isinstance 확인이 자연스럽게 작동했으며, 메타클래스 충돌이 없었습니다. 결괏값은 클래스 구성 중에 올바른 상속 구조로 용해되는 선언적 자리 표시자로서의 팩토리 인스턴스였습니다. 이로 인해 super() 의미론이 보존되고 다중 상속과의 호환성이 유지되었습니다.

결과적으로 개발자들은 class OrderService(DatabaseMixin(postgres_url)):와 같이 작성할 수 있었으며, 올바른 메서드 해석 및 완전한 IDE 지원을 제공하는 연결 풀 및 세션 관리 기능을 자동으로 받을 수 있었습니다. 런타임 오버헤드나 상속 충돌은 없었습니다.

후보자들이 자주 놓치는 점

C3 선형화가 __mro_entries__가 기본을 클래스로 확장할 때 상속 목록의 다른 곳에 이미 존재하는 잠재적 중복을 어떻게 처리하나요?

__mro_entries__가 다른 기본에서 또한 나타나는 클래스를 반환하면 (예를 들어, 한 팩토리가 (BaseA,)로 확장되고, 다른 명시적 기본이 Derived(BaseA)인 경우), Python의 C3 알고리즘은 확장된 튜플을 효과적인 기본 목록으로 취급합니다. 그 후 알고리즘은 이러한 목록을 병합하며 로컬 우선 순위 순서를 보존하고 단조성을 보장합니다. C3는 공통 조상을 처리하도록 설계되었기 때문에, BaseA는 최종 MRO에서 한 번만 나타나며, object 이전에 있지만, 그것에 의존하는 모든 클래스 이후에 위치합니다. 후보자들은 이것이 충돌이나 중복 항목을 생성한다고 잘못 믿는 경우가 많지만, 선형화 과정은 자연스럽게 중복 제거를 수행하면서 "자식이 부모보다 먼저"라는 제약 조건을 유지하여 일관된 메서드 해석을 보장합니다.

__mro_entries__가 생성 중인 클래스에 접근할 수 없으며, 이를 시도할 경우 어떤 특정 오류가 발생하나요?

클래스 생성 중에 type.__new__는 클래스 객체가 인스턴스화되기 전에 기본 객체에서 __mro_entries__를 호출합니다. 네임스페이스 딕셔너리는 존재하지만, 클래스 객체는 아직 정체성을 가지지 않습니다. 구현이 잠재적 클래스의 속성을 접근하려고 시도하면 (예를 들어, 외부 범위에서 클래스 이름을 참조하거나 새로운 클래스에 바인딩되었다고 가정하여 bases를 검사하려고 시도하는 경우), NameErrorAttributeError가 발생합니다. 후보자들은 종종 클래스의 최종 상태나 __dict__를 검사하여 동적 결정을 내릴 수 있다고 가정하지만, 메서드는 오직 원래 기본의 튜플을 인자로 받아야 하고, 반환 값을 결정하기 위해 자신의 내부 상태에만 의존해야 합니다.

__mro_entries__ABC의 가상 하위 클래스로 등록되었을 때 ABC가 MRO에 나타나게 하나요?

아니요. 가상 하위 클래스 등록은 isinstance()issubclass() 확인을 위해 ABC 내의 내부 캐시를 채우는 런타임 메커니즘입니다. 이는 서브클래스의 __mro__ 속성을 변경하지 않습니다. MyClass(MyObject())가 정의되고, MyObject()__mro_entries__를 통해 (ConcreteBase,)를 반환할 때, 오직 ConcreteBase만이 MyClass.__mro__에 나타납니다. 만약 ConcreteBaseMyABC의 가상 하위 클래스로 등록된다면, isinstance(MyClass(), MyABC)True를 반환하지만, MyABCMyClass.__mro__에 존재하지 않습니다. 후보자들은 종종 가상 하위 클래스화를 진정한 상속과 혼동하여, super() 호출이나 MRO 검사에서 ABC 관계가 반영되지 않거나, ABC에서 정의된 메서드가 상속을 통해 사용 가능하지 않은 이유에 대해 혼란을 느낍니다.