[Deferred] PEP 447 - Add getdescriptor method to metaclass
원문 링크: PEP 447 - Add getdescriptor method to metaclass
상태: Deferred 유형: Standards Track 작성일: 12-Jun-2013
PEP 447 – 메타클래스(Metaclass)에 __getdescriptor__
메서드 추가 제안
- 작성자: Ronald Oussoren
- 상태: 연기됨 (Deferred)
- 유형: 표준 트랙 (Standards Track)
- 생성일: 2013년 6월 12일
요약 (Abstract)
현재 object.__getattribute__
및 super.__getattribute__
는 속성(attribute)을 찾을 때 MRO(Method Resolution Order) 내 클래스의 __dict__
를 직접 참조합니다. 이 PEP는 메타클래스(metaclass)에 선택적(optional)인 __getdescriptor__
메서드를 추가하여 이 동작을 대체하고, 특히 super
객체를 사용할 때 속성 조회(attribute lookup)에 더 많은 제어권을 부여하는 것을 목표로 합니다.
제안된 변경 사항은 _PyType_Lookup
및 super.__getattribute__
의 MRO 순회(walking) 루프를 수정하여, 클래스 __dict__
를 직접 확인하는 대신 cls.__getdescriptor__(name)
을 호출하도록 합니다. __getdescriptor__
의 기본 구현은 클래스 딕셔너리(__dict__
)를 조회합니다.
PEP 상태 (PEP Status)
이 PEP는 누군가 이 PEP를 업데이트하고 진행할 시간을 가질 때까지 연기(deferred)되었습니다.
제안 배경 (Rationale)
현재 super
클래스가 속성을 조회하는 방식(super.__getattribute__
가 무조건 클래스의 __dict__
를 참조함)에 영향을 미칠 수 없습니다. 이는 필요에 따라 새로운 메서드를 동적으로 추가할 수 있는 동적 클래스(dynamic classes), 예를 들어 동적 프록시 클래스(dynamic proxy classes)에 문제가 될 수 있습니다.
__getdescriptor__
메서드를 통해 super
클래스를 사용하여 속성을 조회할 때도 동적으로 속성을 추가할 수 있게 됩니다.
새로운 메서드는 일관성을 유지하고 클래스를 위한 동적 속성 해결(dynamic attribute resolution)을 구현하는 단일 지점을 제공하기 위해 object.__getattribute__
(및 PyObject_GenericGetAttr
)에도 영향을 미칩니다.
상세 배경 (Background)
super.__getattribute__
의 현재 동작은 다른 (파이썬이 아닌) 클래스 또는 타입을 위한 동적 프록시(dynamic proxies) 역할을 하는 클래스(예: PyObjC)에 문제를 일으킵니다. PyObjC는 Objective-C 런타임의 모든 클래스에 대해 파이썬 클래스를 생성하고, 메서드가 사용될 때 Objective-C 런타임에서 메서드를 조회합니다. 이는 일반적인 접근 방식에서는 잘 작동하지만, super
객체를 통한 접근에서는 작동하지 않습니다.
이러한 이유로 PyObjC는 현재 자체 클래스와 함께 사용해야 하는 사용자 정의 super
를 포함하고 있으며, 일반 속성 접근을 위해 PyObject_GenericGetAttr
를 완전히 재구현하고 있습니다. 이 PEP의 API를 통해 사용자 정의 super
를 제거하고, 사용자 정의 조회 동작을 중앙 집중화된 위치에 추가할 수 있어 구현이 단순화됩니다.
참고: Objective-C 클래스는 런타임에 새로운 메서드를 추가할 수 있기 때문에 PyObjC는 클래스 __dict__
의 내용을 미리 계산할 수 없습니다. 또한, Objective-C 클래스는 많은 메서드를 포함하는 경향이 있지만 대부분의 파이썬 코드는 그 중 일부만 사용하므로 미리 계산하는 것은 불필요하게 비용이 많이 듭니다.
슈퍼클래스 속성 조회 훅 (The superclass attribute lookup hook)
super.__getattribute__
와 object.__getattribute__
(또는 C 코드의 PyObject_GenericGetAttr
및 특히 _PyType_Lookup
) 모두 객체의 MRO를 순회하며 현재는 클래스의 __dict__
를 직접 참조하여 속성을 찾습니다.
이 제안을 통해 두 조회 메서드는 더 이상 클래스 __dict__
를 직접 참조하지 않고, 메타클래스에 정의된 슬롯(slot)인 특수 메서드 __getdescriptor__
를 호출합니다. 이 메서드의 기본 구현은 클래스 __dict__
에서 이름을 조회하므로, 메타타입(metatype)이 새로운 특수 메서드를 실제로 정의하지 않는 한 속성 조회는 변경되지 않습니다.
파이썬에서의 속성 해결 알고리즘 (Attribute resolution algorithm in Python)
object.__getattribute__
(또는 CPython 구현의 PyObject_GenericGetAttr
)에 의해 구현된 속성 해결(attribute resolution) 프로세스는 비교적 간단하지만, C 코드를 읽지 않고는 완전히 이해하기 어렵습니다.
이 PEP는 “# PEP 447
“으로 시작하는 줄에서 __dict__
조회를 메서드 호출로 변경하여 실제 조회를 수행하도록 합니다. 이를 통해 일반 속성 접근 및 super
프록시를 통한 접근 모두에서 조회에 영향을 미칠 수 있습니다.
참고: 특정 클래스는 이미 자체 __getattribute__
슬롯을 구현하여 기본 동작을 완전히 오버라이드(override)할 수 있습니다 (슈퍼클래스 구현 호출 여부와 관계없이).
파이썬 코드 예시
메타타입은 super.__getattribute__
와 object.__getattribute__
모두에서 속성 해결 중에 호출되는 __getdescriptor__
메서드를 정의할 수 있습니다.
class MetaType(type):
def __getdescriptor__(cls, name):
try:
return cls.__dict__[name]
except KeyError:
raise AttributeError(name) from None
__getdescriptor__
메서드는 인자로 클래스(메타타입의 인스턴스)와 조회되는 속성의 이름(name
)을 받습니다. 이 메서드는 디스크립터(descriptor)를 호출하지 않고 속성 값을 반환해야 하며, 이름을 찾을 수 없으면 AttributeError
를 발생시켜야 합니다. type
클래스는 클래스 딕셔너리에서 이름을 조회하는 __getdescriptor__
의 기본 구현을 제공합니다.
예시 사용 (Example usage)
아래 코드는 속성 조회를 이름의 대문자 버전으로 리디렉션하는 간단한 메타클래스를 구현합니다.
class UpperCaseAccess (type):
def __getdescriptor__(cls, name):
try:
return cls.__dict__[name.upper()]
except KeyError:
raise AttributeError(name) from None
class SillyObject (metaclass=UpperCaseAccess):
def m(self):
return 42
def M(self):
return "fortytwo"
obj = SillyObject()
assert obj.m() == "fortytwo" # obj.m()은 실제로는 obj.M()을 호출하게 됩니다.
이 PEP에서 이전에 언급했듯이, 이 기능의 보다 현실적인 사용 사례는 속성 접근을 기반으로 클래스 __dict__
를 동적으로 채우는 __getdescriptor__
메서드입니다. 이는 __dict__
를 채우는 데 사용되는 소스(source)도 동적이고, 변경 사항을 감지하는 데 사용할 수 있는 트리거(trigger)가 없어 클래스 __dict__
를 소스와 안정적으로 동기화할 수 없을 때 특히 유용합니다.
PyObjC의 클래스 브리지(class bridges)가 그 예시입니다. 클래스 브리지는 Objective-C 클래스를 나타내는 파이썬 객체(클래스)이며, 개념적으로 Objective-C 클래스의 모든 Objective-C 메서드에 대한 파이썬 메서드를 가집니다. 파이썬과 마찬가지로 Objective-C 클래스에 새로운 메서드를 추가하거나 기존 메서드를 대체할 수 있으며, 이를 감지하는 데 사용할 수 있는 콜백(callbacks)은 없습니다.
C 코드에서 (In C code)
새로운 슬롯이 존재하며 사용되어야 함을 나타내는 Py_TPFLAGS_GETDESCRIPTOR
플래그가 추가됩니다.
PyTypeObject
구조체에 __getdescriptor__
메서드에 해당하는 새로운 슬롯 tp_getdescriptor
가 추가됩니다.
이 슬롯은 다음 프로토타입(prototype)을 가집니다.
PyObject* (*getdescriptorfunc)(PyTypeObject* cls, PyObject* name);
이 메서드는 슈퍼클래스를 보지 않고 cls
의 네임스페이스(namespace)에서 name
을 찾아야 하며, 디스크립터를 호출해서는 안 됩니다. 이름을 찾을 수 없는 경우 예외를 설정하지 않고 NULL
을 반환하고, 그렇지 않으면 새 참조(new reference)를 반환합니다 (빌린 참조가 아님). tp_getdescriptor
슬롯이 있는 클래스는 새로운 슬롯이 사용되어야 함을 나타내기 위해 tp_flags
에 Py_TPFLAGS_GETDESCRIPTOR
를 추가해야 합니다.
인터프리터에 의한 훅 사용 (Use of this hook by the interpreter)
새로운 메서드는 메타타입에 필요하며, 따라서 type_
에 정의됩니다. super.__getattribute__
와 object.__getattribute__
/PyObject_GenericGetAttr
( _PyType_Lookup
을 통해) 모두 MRO를 순회할 때 이 __getdescriptor__
메서드를 사용합니다.
구현의 다른 변경 사항 (Other changes to the implementation)
PyObject_GenericGetAttr
에 대한 변경 사항은 private 함수 _PyType_Lookup
을 변경하여 이루어집니다. 이 함수는 현재 빌린 참조(borrowed reference)를 반환하지만, __getdescriptor__
메서드가 있을 때는 새 참조(new reference)를 반환해야 합니다. 이로 인해 _PyType_Lookup
은 _PyType_LookupName
으로 이름이 변경되며, 이는 이 private API의 모든 외부(out-of-tree) 사용자에게 컴파일 타임(compile-time) 오류를 일으킬 것입니다.
같은 이유로 _PyType_LookupId
는 _PyType_LookupId2
로 이름이 변경됩니다. 동일한 문제가 있는 typeobject.c
의 다른 여러 함수는 해당 파일에 private하기 때문에 업데이트된 이름을 얻지 못합니다.
Objects/typeobject.c
의 속성 조회 캐시(attribute lookup cache)는 __getdescriptor__
를 오버라이드하는 메타클래스를 가진 클래스에 대해 비활성화됩니다. 이는 캐시를 사용하는 것이 이러한 클래스에 유효하지 않을 수 있기 때문입니다.
PEP가 인트로스펙션(Introspection)에 미치는 영향
이 PEP에서 도입된 메서드의 사용은 사용자 정의 __getdescriptor__
메서드를 사용하는 메타클래스를 가진 클래스의 인트로스펙션에 영향을 미칠 수 있습니다.
아래 나열된 항목은 사용자 정의 __getdescriptor__
메서드에 의해서만 영향을 받습니다. object
의 기본 구현은 여전히 클래스 __dict__
만을 사용하고 object.__getattribute__
의 가시적인 동작에 눈에 띄는 변화를 일으키지 않으므로 문제가 발생하지 않습니다.
dir()
이 모든 속성을 표시하지 않을 수 있음: 사용자 정의__getattribute__
메서드와 마찬가지로,__getdescriptor__()
메서드를 사용하여 속성을 동적으로 해결할 때dir()
이 모든 (인스턴스) 속성을 보지 못할 수 있습니다. 해결책은 간단합니다.__getdescriptor__
를 사용하는 클래스는 내장dir()
함수에 대한 완전한 지원을 원한다면__dir__()
도 구현해야 합니다.inspect.getattr_static
이 모든 속성을 표시하지 않을 수 있음:inspect.getattr_static
함수는 이 함수로 인트로스펙션하는 동안 사용자 코드를 호출하는 것을 피하기 위해 의도적으로__getattribute__
및 디스크립터를 호출하지 않습니다.__getdescriptor__
메서드도 무시되며, 이는inspect.getattr_static
의 결과가builtin.getattr
와 다를 수 있는 또 다른 방식입니다.inspect.getmembers
및inspect.classify_class_attrs
: 이 두 함수는 모두 MRO를 따라 클래스의__dict__
에 직접 접근하므로, 사용자 정의__getdescriptor__
메서드에 의해 영향을 받을 수 있습니다. 사용자 정의__getdescriptor__
메서드를 가진 코드가 이러한 메서드와 잘 작동하려면,__dict__
가 파이썬 코드에 의해 직접 접근될 때 올바르게 설정되었는지 확인해야 합니다.inspect.getmembers
는pydoc
에서 사용되므로, 런타임 문서 인트로스펙션에 영향을 미칠 수 있습니다.- 클래스
__dict__
의 직접적인 인트로스펙션: 인트로스펙션을 위해 클래스__dict__
에 직접 접근하는 모든 코드는 사용자 정의__getdescriptor__
메서드에 의해 영향을 받을 수 있습니다.
성능 영향 (Performance impact)
경고: 이 섹션의 벤치마크 결과는 오래되었으며, 패치가 현재 트렁크에 포팅(ported)되면 업데이트될 예정입니다. 이 섹션의 결과에 중요한 변화가 있을 것으로 예상하지는 않습니다.
마이크로 벤치마크 (Micro benchmarks)
Issue 18181에는 속성 조회 속도를 직접적으로 그리고 super
를 통해 테스트하는 마이크로 벤치마크(pep447-micro-bench.py
)가 첨부되어 있습니다.
사용자 정의 __getdescriptor__
메서드를 사용할 경우 깊은 클래스 계층 구조에서의 속성 조회가 상당히 느려집니다. 이는 이 메서드가 있을 때 CPython의 속성 조회 캐시를 사용할 수 없기 때문입니다.
Pybench
제안된 변경 사항의 성능 영향은 미미한 것으로 나타났습니다. 여러 벤치마크에서 약간의 성능 저하 또는 향상이 있었지만, 전반적인 총 시간(Totals)은 거의 변화가 없었습니다 (-0.1%
변화).
대안 제안 (Alternative proposals)
__getattribute_super__
이 PEP의 초기 버전은 클래스에 다음 정적 메서드를 사용했습니다.
def __getattribute_super__(cls, name, object, owner):
pass
이 메서드는 이름 조회 및 디스크립터 호출을 수행했으며, super.__getattribute__
와만 작동하도록 제한되었습니다.
tp_getattro
재사용 (Reuse tp_getattro)
새로운 슬롯을 추가하는 것을 피하고 API를 더 간단하고 이해하기 쉽게 유지하는 것이 좋다는 의견이 있었습니다. Issue 18181의 한 댓글은 tp_getattro
슬롯을 재사용하는 것에 대해 질문했습니다. 즉, super
가 MRO를 따라 모든 메서드의 tp_getattro
슬롯을 호출할 수 있는지에 대한 질문이었습니다.
이는 작동하지 않습니다. 왜냐하면 tp_getattro
는 MRO의 클래스를 사용하여 속성을 해결하려고 시도하기 전에 인스턴스 __dict__
를 조회하기 때문입니다. 이는 클래스 딕셔너리를 직접 참조하는 대신 tp_getattro
를 사용하는 것이 super
클래스의 의미론(semantics)을 변경한다는 것을 의미합니다.
새로운 메서드의 대안적인 배치 (Alternative placement of the new method)
이 PEP는 __getdescriptor__
를 메타클래스의 메서드로 추가할 것을 제안합니다. 대안으로는 이를 클래스 자체의 클래스 메서드로 추가하는 것입니다 ( __new__
가 클래스의 정적 메서드이고 메타클래스의 메서드가 아닌 것과 유사).
메타클래스에 메서드를 사용하는 장점은 MRO에 있는 두 클래스가 __getdescriptor__
에 대해 다른 동작을 가질 수 있는 다른 메타클래스를 가질 때 오류가 발생한다는 것입니다. 일반적인 클래스 메서드의 경우, 이러한 문제는 감지되지 않고 넘어갈 수 있으며, 코드를 실행할 때 미묘한 오류를 유발할 수 있습니다.
역사 (History)
- 2015년 7월 23일: Guido와 논의 후
Py_TPFLAGS_GETDESCRIPTOR
타입 플래그가 추가되었습니다. 이 새로운 플래그는 주로 이전 버전의 CPython용 확장 프로그램을 로드할 때 충돌을 피하는 데 유용하며, 속도 향상에도 긍정적인 영향을 미칠 수 있습니다. - 2014년 7월: 슬롯 이름이
__getdescriptor__
로 변경되었습니다. 이전 이름은 다른 슬롯의 명명 규칙과 일치하지 않았고 덜 설명적이었습니다.
토론 스레드 및 참고 자료 (Discussion threads and References)
- 초기 버전의 PEP는 특정 Message-ID를 가진 메시지로 전송되었습니다.
- 이후 Message-ID를 가진 메시지들에서 추가 토론이 이어졌습니다.
- Issue 18181에는 오래된 프로토타입 구현이 포함되어 있습니다.
저작권 (Copyright)
이 문서는 퍼블릭 도메인(public domain)에 있습니다.
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments