Python프로그래밍수석 Python 개발자

어떻게 **Python**의 제로 인자 `super()`가 여러 하위 클래스에 의해 상속된 메서드에 대해 정의 클래스를 올바르게 참조할 수 있을까요?

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

질문에 대한 답변

제로 인자 super()Python 클래스 본문 내에서 정의된 모든 메서드에 대해 암묵적으로 생성되는 컴파일러 생성 클로저 셀 __class__에 의존합니다. 컴파일러가 클래스 정의를 처리할 때, 정의 중인 클래스 객체를 가리키는 셀 변수 __class__를 메서드의 클로저에 생성합니다. 인수 없이 super()가 호출되면 C 구현은 호출 프레임을 검사하여 이 __class__ 셀을 찾아 첫 번째 인자(유형)로 사용합니다. 그런 다음 메서드의 첫 번째 위치 인자(일반적으로 self)를 인스턴스로 사용합니다. 이 메커니즘은 정의 시간에 클래스 참조를 바인딩하여 호출 시간이 아닌 정의 시간에 클래스 이름을 하드코딩할 필요성을 없애며, 상속 체인의 각 메서드가 자신의 MRO(메서드 해결 순서)에서 특정 위치를 참조하도록 보장합니다.

class Base: def method(self): return "Base" class Middle(Base): def method(self): # __class__는 여기서 Middle에 바인딩됩니다. return f"Middle -> {super().method()}" class Derived(Middle): def method(self): # __class__는 여기서 Derived에 바인딩됩니다. return f"Derived -> {super().method()}"

실제 상황

우리는 깊은 계층 구조의 가격 모델을 가진 정량적 거래 라이브러리를 유지 관리했습니다. BaseModelcalculate_risk() 메서드를 제공하고, EquityModel은 주식 특정 로직을 추가하기 위해 이를 재정의했으며, AmericanOptionModel은 이를 더욱 전문화했습니다. EquityModel의 이름을 VanillaEquityModel로 변경하는 대규모 리팩토링을 진행하는 동안, 복사-붙여넣기 된 믹스인 클래스에서 수십 개의 오래된 super(EquityModel, self) 호출을 발견했습니다. 이러한 오래된 참조는 TypeError 또는 잘못된 부모 메서드가 호출되어 생산 리스크 계산을 중단시키는 조용한 논리 오류를 발생시켰습니다.

해결책 1: 전역 검색 및 교체 리팩토링. 우리는 200,000 라인의 코드베이스 전역에서 모든 하드코딩된 클래스 이름을 찾아서 교체하기 위해 자동화 도구를 사용하는 것을 고려했습니다. 장점: 아키텍처 변경이 필요하지 않으며, 레거시 Python 2 구문과 함께 작동합니다. 단점: 깨지기 쉽고 불완전하며, 동적으로 생성된 클래스, 문자열 기반 동적 메서드 할당 및 서드 파티 확장에서의 참조를 놓칩니다. 또한 각 메서드마다 클래스 이름이 반복되므로 DRY 원칙을 위반합니다.

해결책 2: 제로 인자 super()의 보편적 채택. 우리는 전체 코드베이스를 인수 없이 super()를 사용하도록 마이그레이션했습니다. 장점: 이는 클래스 이름 변경을 완전히 안전하게 만들고, 리팩토링 동안의 주요 인적 오류 출처를 없애며, 불필요한 소음을 제거하여 가독성을 크게 향상시킵니다. 또한 복잡한 협력 다중 상속 패턴을 올바르게 처리합니다. 단점: Python 3.6 이상이 필요하며(우리는 이미 가지고 있었습니다), 암묵적 클로저 메커니즘에 익숙하지 않은 개발자들은 처음에는 혼란스러워했습니다. 또한 정의 이후에 클래스에 동적으로 연결된 함수에서는 사용할 수 없습니다.

해결책 3: 클래스 참조의 메타클래스 주입. 우리는 모든 메서드에 _defining_class 속성을 주입하기 위해 메타클래스를 사용하는 것을 잠시 고려했습니다. 장점: 이는 메커니즘을 명시적이고 검사 가능하게 만듭니다. 단점: 이는 상당한 복잡성과 부하를 추가하고, 표준 CPython 최적화와 충돌하며, 언어 컴파일러에 의해 이미 제공된 기능을 재발명합니다.

우리는 해결책 2를 선택했습니다. 마이그레이션은 한 스프린트 동안 완료되었습니다. 그 결과로 클래스 이름 변경과 관련된 이후의 리팩토링 작업에 소요되는 시간이 40% 감소하였고, CI 파이프라인에서의 오래된 super() 참조와 관련된 전체 버그 범주가 제거되었습니다.

후보자들이 자주 놓치는 것

super()가 제로 인자로 호출될 때 물리적으로 __class__ 셀을 어떻게 찾는가?

CPython에서 super()의 구현(Objects/typeobject.c)은 PyEval_GetLocals()를 사용하여 호출 프레임의 로컬 변수와 클로저를 검사합니다. 특히 __class__라는 자유 변수(셀)를 찾습니다. 이 셀은 컴파일러가 클래스 본문 내에서 어휘적으로 정의된 함수에 대해 생성합니다(CO_OPTIMIZED 플래그 및 클래스 범위에 의해 표시됨). 셀이 발견되면 super()는 클래스 객체를 추출하고, 발견되지 않으면 RuntimeError: super(): __class__ cell not found를 발생시킵니다. 제로 인자 형식은 기본적으로 컴파일러에 의해 super(__class__, self)로 변환되며, 여기서 __class__는 닫힌 변수입니다.

클래스 생성 이후 클래스 속성에 할당된 함수 내에서 제로 인자 super()를 사용하려고 하면 어떻게 됩니까?

클래스 본문 외부에서 함수를 정의한 다음 이를 메서드로 할당하면(예: MyClass.method = some_function), 해당 함수 내에서 super()를 호출하면 RuntimeError가 발생합니다. 이는 컴파일러가 클래스 구문 일부로 컴파일된 코드 객체에 대해만 __class__ 셀을 생성하기 때문입니다. 셀이 없으면 super()는 계층 구조에서 어떤 클래스가 "현재" 클래스인지 결정할 수 있는 방법이 없습니다. 함수 정의 범위와 해당 함수가 나중에 연결된 클래스를 구분할 수 없습니다.

하위 클래스 메서드가 super()를 호출하고 부모 메서드도 super()를 호출할 때 무한 재귀가 발생하지 않는 이유는 무엇입니까?

이것은 __class__가 메서드가 정의된 클래스를 참조하고, 아니라면 인스턴스의 런타임 클래스(type(self)가 아닌) 때문입니다. Derived.method()super()를 호출하면 __class__Derived임을 감지하고 Derived.__mro__의 다음 클래스(예: Middle)로 위임합니다. Middle.method()에 도달하면 super()를 호출하고, 자신의 고유한 __class__ 셀은 Middle을 포함하고 있으므로, Middle 다음의 다음 클래스를 찾습니다(예: Base). 계층의 각 레벨은 자신의 정의 시간 클래스 참조를 사용하므로 MRO가 선형으로 위쪽으로 정확하게 한 번만 순회되며 하위 클래스로 되돌아가지 않습니다.