[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_Lookupsuper.__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_flagsPy_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.getmembersinspect.classify_class_attrs: 이 두 함수는 모두 MRO를 따라 클래스의 __dict__에 직접 접근하므로, 사용자 정의 __getdescriptor__ 메서드에 의해 영향을 받을 수 있습니다. 사용자 정의 __getdescriptor__ 메서드를 가진 코드가 이러한 메서드와 잘 작동하려면, __dict__가 파이썬 코드에 의해 직접 접근될 때 올바르게 설정되었는지 확인해야 합니다. inspect.getmemberspydoc에서 사용되므로, 런타임 문서 인트로스펙션에 영향을 미칠 수 있습니다.
  • 클래스 __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에는 오래된 프로토타입 구현이 포함되어 있습니다.

이 문서는 퍼블릭 도메인(public domain)에 있습니다.

⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.

Comments