Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 447 – 메타클래스에 __getdescriptor__ 메서드 추가

Author:
Ronald Oussoren <ronaldoussoren at mac.com>
Status:
Deferred
Type:
Standards Track
Created:
12-Jun-2013
Post-History:
02-Jul-2013, 15-Jul-2013, 29-Jul-2013, 22-Jul-2015

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

현재 object.__getattribute__super.__getattribute__는 속성을 찾을 때 클래스의 MRO에 있는 클래스들의 __dict__를 살펴봅니다. 이 PEP에서는 메타클래스에 선택적인 __getdescriptor__ 메서드를 추가하여 이 동작을 대체하고, 특히 super 객체를 사용할 때 속성 조회를 더 세밀하게 제어할 수 있도록 합니다.

즉, _PyType_Lookupsuper.__getattribute__에 있는 MRO 순회 루프가 다음에서 변경됩니다.:

def lookup(mro_list, name):
    for cls in mro_list:
        if name in cls.__dict__:
            return cls.__dict__

    return NotFound

다음으로:

def lookup(mro_list, name):
    for cls in mro_list:
        try:
            return cls.__getdescriptor__(name)
        except AttributeError:
            pass

    return NotFound

__getdescriptor__의 기본 구현은 클래스 딕셔너리를 살펴봅니다.:

class type:
   def __getdescriptor__(cls, name):
       try:
           return cls.__dict__[name]
       except KeyError:
           raise AttributeError(name) from None

PEP 상태

이 PEP는 누군가 이 PEP를 갱신하고 추진할 시간이 생길 때까지 보류됩니다.

근거

현재 super class가 속성을 조회하는 방식을 제어할 수 없습니다(즉, super.__getattribute__는 무조건 클래스의 __dict__를 살펴봅니다). 이는 필요에 따라 새 메서드를 추가할 수 있는 동적 클래스, 예를 들어 동적 프록시 클래스에 문제가 될 수 있습니다.

__getdescriptor__ 메서드를 사용하면 super class를 통해 속성을 조회할 때에도 속성을 동적으로 추가할 수 있습니다.

새 메서드는 일관성을 유지하고 클래스의 동적 속성 해석을 구현할 단일 위치를 제공하기 위해 object.__getattribute__(및 PyObject_GenericGetAttr)에도 영향을 줍니다.

배경

현재 super.__getattribute__의 동작은 다른 (Python이 아닌) 클래스 또는 타입의 동적 프록시인 클래스에 문제를 일으키며, 그 예로 PyObjC를 들 수 있습니다. PyObjC는 Objective-C 런타임의 각 클래스에 대해 Python 클래스를 생성하고, 메서드가 사용될 때 Objective-C 런타임에서 해당 메서드를 조회합니다. 이는 일반적인 접근에서는 정상적으로 작동하지만, super 객체를 통한 접근에서는 작동하지 않습니다. 이 때문에 PyObjC에는 현재 해당 클래스와 함께 사용해야 하는 사용자 정의 super가 포함되어 있으며, 일반적인 속성 접근을 위해 PyObject_GenericGetAttr 도 완전히 다시 구현해야 합니다.

이 PEP의 API를 사용하면 사용자 정의 super를 제거할 수 있고, 사용자 정의 조회 동작을 중앙 위치에 추가할 수 있으므로 구현도 단순해집니다.

Note

PyObjC 는 Objective-C 클래스가 런타임에 새 메서드를 추가할 수 있기 때문에 클래스 __dict__의 내용을 미리 계산할 수 없습니다. 또한 Objective-C 클래스에는 많은 메서드가 포함되는 경향이 있는 반면 대부분의 Python 코드는 그중 일부만 사용하므로, 이를 미리 계산하면 불필요하게 비용이 많이 듭니다.

슈퍼클래스 속성 조회 훅

super.__getattribute__object.__getattribute__(또는 PyObject_GenericGetAttr 및 특히 _PyType_Lookup를 C 코드에서 사용)는 객체의 MRO를 순회하며 현재 속성을 조회하기 위해 클래스의 __dict__를 살펴봅니다.

이 제안에 따르면 두 조회 메서드는 더 이상 클래스의 __dict__를 살펴보지 않고, 메타클래스에 정의된 슬롯인 특수 메서드 __getdescriptor__를 호출합니다. 해당 메서드의 기본 구현은 클래스 __dict__에서 이름을 조회하므로, 메타타입이 실제로 새 특수 메서드를 정의하지 않는 한 속성 조회는 변경되지 않습니다.

참고: Python의 속성 해석 알고리즘

object.__getattribute__ (또는 CPython 구현의 PyObject_GenericGetAttr)가 구현하는 속성 해석 과정은 상당히 간단하지만, C 코드를 읽지 않으면 완전히 그렇지는 않습니다.

현재 CPython의 object.__getattribute__ 구현은 기본적으로 다음 (의사) Python 코드와 동등합니다(일부 정리 작업과 속도 최적화 기법은 제외합니다).:

def _PyType_Lookup(tp, name):
    mro = tp.mro()
    assert isinstance(mro, tuple)

    for base in mro:
       assert isinstance(base, type)

       # PEP 447 will change these lines:
       try:
           return base.__dict__[name]
       except KeyError:
           pass

    return None


class object:
    def __getattribute__(self, name):
        assert isinstance(name, str)

        tp = type(self)
        descr = _PyType_Lookup(tp, name)

        f = None
        if descr is not None:
            f = descr.__get__
            if f is not None and descr.__set__ is not None:
                # Data descriptor
                return f(descr, self, type(self))

        dict = self.__dict__
        if dict is not None:
            try:
                return self.__dict__[name]
            except KeyError:
                pass

        if f is not None:
            # Non-data descriptor
            return f(descr, self, type(self))

        if descr is not None:
            # Regular class attribute
            return descr

        raise AttributeError(name)


class super:
    def __getattribute__(self, name):
       assert isinstance(name, unicode)

       if name != '__class__':
           starttype = self.__self_type__
           mro = startype.mro()

           try:
               idx = mro.index(self.__thisclass__)

           except ValueError:
               pass

           else:
               for base in mro[idx+1:]:
                   # PEP 447 will change these lines:
                   try:
                       descr = base.__dict__[name]
                   except KeyError:
                       continue

                   f = descr.__get__
                   if f is not None:
                       return f(descr,
                           None if (self.__self__ is self.__self_type__) else self.__self__,
                           starttype)

                   else:
                       return descr

       return object.__getattribute__(self, name)

이 PEP에서는 “# PEP 447”로 시작하는 줄의 딕셔너리 조회를 실제 조회를 수행하는 메서드 호출로 변경하여, 일반적인 속성 접근과 super proxy를 통한 접근 모두에서 해당 조회에 영향을 줄 수 있도록 해야 합니다.

특정 클래스는 자체 __getattribute__ 슬롯을 구현하여 이미 기본 동작을 완전히 재정의할 수 있다는 점에 유의하십시오(슈퍼클래스 구현을 호출하는지 여부와 관계없이 가능합니다).

Python 코드에서

메타타입은 __getdescriptor__ 메서드를 정의할 수 있으며, 이 메서드는 super.__getattribute__object.__getattribute가 속성을 해석하는 동안 호출됩니다.:

class MetaType(type):
    def __getdescriptor__(cls, name):
        try:
            return cls.__dict__[name]
        except KeyError:
            raise AttributeError(name) from None

__getdescriptor__ 메서드는 메타 타입의 인스턴스인 클래스와 조회되는 속성의 이름을 인자로 가집니다. 디스크립터를 호출하지 않고 속성의 값을 반환해야 하며, 이름을 찾을 수 없을 때는 AttributeError를 발생시켜야 합니다.

type 클래스는 클래스 딕셔너리에서 이름을 조회하는 __getdescriptor__의 기본 구현을 제공합니다.

사용 예

아래 코드는 속성 조회를 이름의 대문자 버전으로 리디렉션하는 단순한 메타클래스를 구현합니다.:

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"

이 PEP에서 앞서 언급했듯이, 이 기능의 보다 현실적인 사용 사례는 속성 접근을 기반으로 클래스 __dict__를 동적으로 채우는 __getdescriptor__ 메서드입니다. 이는 주로 클래스 딕셔너리를 소스와 안정적으로 동기화할 수 없는 경우에 사용되며, 예를 들어 __dict__를 채우는 데 사용되는 소스 자체도 동적이고 해당 소스의 변경을 감지하는 데 사용할 트리거가 없는 경우가 이에 해당합니다.

그 예로 PyObjC의 클래스 브리지가 있습니다. 클래스 브리지는 Objective-C 클래스를 나타내는 Python 객체(클래스)이며, 개념적으로 Objective-C 클래스의 모든 Objective-C 메서드에 대응하는 Python 메서드를 가집니다. Python과 마찬가지로 Objective-C 클래스에 새 메서드를 추가하거나 기존 메서드를 교체할 수 있지만, 이를 감지하는 데 사용할 수 있는 콜백은 없습니다.

C 코드에서

새로운 슬롯이 존재하며 사용되어야 함을 나타내는 값 (1UL << 11)의 새로운 타입 플래그 Py_TPFLAGS_GETDESCRIPTOR가 추가됩니다.

PyTypeObject 구조체에 새로운 슬롯 tp_getdescriptor가 추가되며, 이 슬롯은 type__getdescriptor__ 메서드에 대응합니다.

슬롯은 다음 프로토타입을 가집니다.:

PyObject* (*getdescriptorfunc)(PyTypeObject* cls, PyObject* name);

이 메서드는 슈퍼클래스를 조회하지 않고 cls의 네임스페이스에서 name을 조회해야 하며, 디스크립터를 호출해서는 안 됩니다. 이름 name을 찾을 수 없을 때 이 메서드는 예외를 설정하지 않고 NULL을 반환하며, 그렇지 않으면 새 참조를 반환합니다(빌린 참조가 아님).

tp_getdescriptor 슬롯이 있는 클래스는 새 슬롯을 사용해야 함을 나타내기 위해 Py_TPFLAGS_GETDESCRIPTORtp_flags에 추가해야 합니다.

인터프리터에서 이 훅 사용

새로운 메서드는 메타 타입에 필요하므로 type_에 정의됩니다. super.__getattribute__object.__getattribute__/PyObject_GenericGetAttr 는(_PyType_Lookup을 통해) MRO를 순회할 때 이 __getdescriptor__ 메서드를 사용합니다.

구현에 대한 기타 변경 사항

PyObject_GenericGetAttr 의 변경은 비공개 함수 _PyType_Lookup을 변경하여 수행됩니다. 이 함수는 현재 빌린 참조를 반환하지만, __getdescriptor__ 메서드가 있을 때는 새 참조를 반환해야 합니다. 따라서 _PyType_Lookup_PyType_LookupName으로 이름이 변경되며, 이로 인해 이 비공개 API를 사용하는 모든 트리 외부 사용자는 컴파일 시 오류를 받게 됩니다.

같은 이유로 _PyType_LookupId_PyType_LookupId2로 이름이 변경됩니다. 같은 문제가 있는 typeobject.c의 다른 여러 함수는 해당 파일에 비공개이므로 이름이 업데이트되지 않습니다.

Objects/typeobject.c의 속성 조회 캐시는 __getdescriptor__를 재정의하는 메타클래스를 가진 클래스에서 비활성화됩니다. 이러한 클래스에서는 캐시를 사용하는 것이 유효하지 않을 수 있기 때문입니다.

이 PEP가 인트로스펙션에 미치는 영향

이 PEP에서 도입된 메서드는 사용자 지정 __getdescriptor__ 메서드를 사용하는 메타클래스를 가진 클래스의 인트로스펙션에 영향을 줄 수 있습니다. 이 절에서는 이러한 변경 사항을 나열합니다.

아래에 나열된 항목은 사용자 지정 __getdescriptor__ 메서드의 영향을 받을 뿐입니다. object의 기본 구현은 여전히 클래스 __dict__만 사용하므로 문제가 발생하지 않으며, object.__getattribute__의 눈에 보이는 동작에도 가시적인 변경을 일으키지 않습니다.

  • dir은 모든 속성을 표시하지 않을 수 있습니다.

    사용자 지정 __getattribute__ 메서드와 마찬가지로 dir()__getdescriptor__() 메서드를 사용하여 속성을 동적으로 확인할 때 모든 (인스턴스) 속성을 확인하지 못할 수 있습니다.

    이에 대한 해결책은 매우 간단합니다. __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__ 메서드를 사용하는 코드가 이러한 메서드와 올바르게 호환되려면, Python 코드가 직접 접근할 때 __dict__가 올바르게 설정되어 있는지도 보장해야 합니다.

    inspect.getmemberspydoc에서 사용되므로, 이로 인해 런타임 문서 인트로스펙션에 영향을 줄 수 있다는 점에 유의하십시오.

  • 클래스 __dict__의 직접 인트로스펙션

    인트로스펙션을 위해 클래스 __dict__에 직접 접근하는 코드는 사용자 정의 __getdescriptor__ 메서드의 영향을 받을 수 있습니다. 이전 항목을 참조하십시오.

성능 영향

경고: 이 절의 벤치마크 결과는 오래된 것이며, 패치를 현재 trunk로 포팅하면 업데이트할 예정입니다. 이 절의 결과에는 큰 변화가 없을 것으로 예상합니다.

마이크로 벤치마크

Issue 18181에는 첨부 파일 중 하나로 마이크로 벤치마크(pep447-micro-bench.py)가 포함되어 있으며, 이 벤치마크는 직접 수행하는 경우와 super를 통한 경우 모두에서 속성 조회 속도를 구체적으로 테스트합니다.

사용자 정의 __getdescriptor__ 메서드를 사용할 때 깊은 클래스 계층 구조에서의 속성 조회가 크게 느려진다는 점에 유의하십시오. 이 메서드가 있으면 CPython의 속성 조회 캐시를 사용할 수 없기 때문입니다.

Pybench

아래의 pybench 출력은 changeset a5681f50bae2를 기반으로 하며, 유휴 상태인 머신과 Centos 6.4를 실행하는 Core i7 프로세서에서 실행한 이 PEP의 구현과 일반 소스 트리를 비교합니다.

머신이 유휴 상태였음에도 실행마다 뚜렷한 차이가 있었으며, “최소 시간”의 차이는 -0.1%에서 +1.5%까지 변동했고, “평균 시간” 차이에서도 비슷하지만 약간 더 작은 차이가 나타났습니다.

-------------------------------------------------------------------------------
PYBENCH 2.1
-------------------------------------------------------------------------------
* using CPython 3.4.0a0 (default, Jul 29 2013, 13:01:34) [GCC 4.4.7 20120313 (Red Hat 4.4.7-3)]
* disabled garbage collection
* system check interval set to maximum: 2147483647
* using timer: time.perf_counter
* timer: resolution=1e-09, implementation=clock_gettime(CLOCK_MONOTONIC)

-------------------------------------------------------------------------------
Benchmark: pep447.pybench
-------------------------------------------------------------------------------

    Rounds: 10
    Warp:   10
    Timer:  time.perf_counter

    Machine Details:
       Platform ID:    Linux-2.6.32-358.114.1.openstack.el6.x86_64-x86_64-with-centos-6.4-Final
       Processor:      x86_64

    Python:
       Implementation: CPython
       Executable:     /tmp/default-pep447/bin/python3
       Version:        3.4.0a0
       Compiler:       GCC 4.4.7 20120313 (Red Hat 4.4.7-3)
       Bits:           64bit
       Build:          Jul 29 2013 14:09:12 (#default)
       Unicode:        UCS4


-------------------------------------------------------------------------------
Comparing with: default.pybench
-------------------------------------------------------------------------------

    Rounds: 10
    Warp:   10
    Timer:  time.perf_counter

    Machine Details:
       Platform ID:    Linux-2.6.32-358.114.1.openstack.el6.x86_64-x86_64-with-centos-6.4-Final
       Processor:      x86_64

    Python:
       Implementation: CPython
       Executable:     /tmp/default/bin/python3
       Version:        3.4.0a0
       Compiler:       GCC 4.4.7 20120313 (Red Hat 4.4.7-3)
       Bits:           64bit
       Build:          Jul 29 2013 13:01:34 (#default)
       Unicode:        UCS4


Test                             minimum run-time        average  run-time
                                 this    other   diff    this    other   diff
-------------------------------------------------------------------------------
          BuiltinFunctionCalls:    45ms    44ms   +1.3%    45ms    44ms   +1.3%
           BuiltinMethodLookup:    26ms    27ms   -2.4%    27ms    27ms   -2.2%
                 CompareFloats:    33ms    34ms   -0.7%    33ms    34ms   -1.1%
         CompareFloatsIntegers:    66ms    67ms   -0.9%    66ms    67ms   -0.8%
               CompareIntegers:    51ms    50ms   +0.9%    51ms    50ms   +0.8%
        CompareInternedStrings:    34ms    33ms   +0.4%    34ms    34ms   -0.4%
                  CompareLongs:    29ms    29ms   -0.1%    29ms    29ms   -0.0%
                CompareStrings:    43ms    44ms   -1.8%    44ms    44ms   -1.8%
    ComplexPythonFunctionCalls:    44ms    42ms   +3.9%    44ms    42ms   +4.1%
                 ConcatStrings:    33ms    33ms   -0.4%    33ms    33ms   -1.0%
               CreateInstances:    47ms    48ms   -2.9%    47ms    49ms   -3.4%
            CreateNewInstances:    35ms    36ms   -2.5%    36ms    36ms   -2.5%
       CreateStringsWithConcat:    69ms    70ms   -0.7%    69ms    70ms   -0.9%
                  DictCreation:    52ms    50ms   +3.1%    52ms    50ms   +3.0%
             DictWithFloatKeys:    40ms    44ms  -10.1%    43ms    45ms   -5.8%
           DictWithIntegerKeys:    32ms    36ms  -11.2%    35ms    37ms   -4.6%
            DictWithStringKeys:    29ms    34ms  -15.7%    35ms    40ms  -11.0%
                      ForLoops:    30ms    29ms   +2.2%    30ms    29ms   +2.2%
                    IfThenElse:    38ms    41ms   -6.7%    38ms    41ms   -6.9%
                   ListSlicing:    36ms    36ms   -0.7%    36ms    37ms   -1.3%
                NestedForLoops:    43ms    45ms   -3.1%    43ms    45ms   -3.2%
      NestedListComprehensions:    39ms    40ms   -1.7%    39ms    40ms   -2.1%
          NormalClassAttribute:    86ms    82ms   +5.1%    86ms    82ms   +5.0%
       NormalInstanceAttribute:    42ms    42ms   +0.3%    42ms    42ms   +0.0%
           PythonFunctionCalls:    39ms    38ms   +3.5%    39ms    38ms   +2.8%
             PythonMethodCalls:    51ms    49ms   +3.0%    51ms    50ms   +2.8%
                     Recursion:    67ms    68ms   -1.4%    67ms    68ms   -1.4%
                  SecondImport:    41ms    36ms  +12.5%    41ms    36ms  +12.6%
           SecondPackageImport:    45ms    40ms  +13.1%    45ms    40ms  +13.2%
         SecondSubmoduleImport:    92ms    95ms   -2.4%    95ms    98ms   -3.6%
       SimpleComplexArithmetic:    28ms    28ms   -0.1%    28ms    28ms   -0.2%
        SimpleDictManipulation:    57ms    57ms   -1.0%    57ms    58ms   -1.0%
         SimpleFloatArithmetic:    29ms    28ms   +4.7%    29ms    28ms   +4.9%
      SimpleIntFloatArithmetic:    37ms    41ms   -8.5%    37ms    41ms   -8.7%
       SimpleIntegerArithmetic:    37ms    41ms   -9.4%    37ms    42ms  -10.2%
      SimpleListComprehensions:    33ms    33ms   -1.9%    33ms    34ms   -2.9%
        SimpleListManipulation:    28ms    30ms   -4.3%    29ms    30ms   -4.1%
          SimpleLongArithmetic:    26ms    26ms   +0.5%    26ms    26ms   +0.5%
                    SmallLists:    40ms    40ms   +0.1%    40ms    40ms   +0.1%
                   SmallTuples:    46ms    47ms   -2.4%    46ms    48ms   -3.0%
         SpecialClassAttribute:   126ms   120ms   +4.7%   126ms   121ms   +4.4%
      SpecialInstanceAttribute:    42ms    42ms   +0.6%    42ms    42ms   +0.8%
                StringMappings:    94ms    91ms   +3.9%    94ms    91ms   +3.8%
              StringPredicates:    48ms    49ms   -1.7%    48ms    49ms   -2.1%
                 StringSlicing:    45ms    45ms   +1.4%    46ms    45ms   +1.5%
                     TryExcept:    23ms    22ms   +4.9%    23ms    22ms   +4.8%
                    TryFinally:    32ms    32ms   -0.1%    32ms    32ms   +0.1%
                TryRaiseExcept:    17ms    17ms   +0.9%    17ms    17ms   +0.5%
                  TupleSlicing:    49ms    48ms   +1.1%    49ms    49ms   +1.0%
                   WithFinally:    48ms    47ms   +2.3%    48ms    47ms   +2.4%
               WithRaiseExcept:    45ms    44ms   +0.8%    45ms    45ms   +0.5%
-------------------------------------------------------------------------------
Totals:                          2284ms  2287ms   -0.1%  2306ms  2308ms   -0.1%

(this=pep447.pybench, other=default.pybench)

벤치마크 모음을 옵션 “-b 2n3”과 함께 실행한 결과도 성능 영향이 미미함을 나타내는 것으로 보입니다.:

Report on Linux fangorn.local 2.6.32-358.114.1.openstack.el6.x86_64 #1 SMP Wed Jul 3 02:11:25 EDT 2013 x86_64 x86_64
Total CPU cores: 8

### call_method_slots ###
Min: 0.304120 -> 0.282791: 1.08x faster
Avg: 0.304394 -> 0.282906: 1.08x faster
Significant (t=2329.92)
Stddev: 0.00016 -> 0.00004: 4.1814x smaller

### call_simple ###
Min: 0.249268 -> 0.221175: 1.13x faster
Avg: 0.249789 -> 0.221387: 1.13x faster
Significant (t=2770.11)
Stddev: 0.00012 -> 0.00013: 1.1101x larger

### django_v2 ###
Min: 0.632590 -> 0.601519: 1.05x faster
Avg: 0.635085 -> 0.602653: 1.05x faster
Significant (t=321.32)
Stddev: 0.00087 -> 0.00051: 1.6933x smaller

### fannkuch ###
Min: 1.033181 -> 0.999779: 1.03x faster
Avg: 1.036457 -> 1.001840: 1.03x faster
Significant (t=260.31)
Stddev: 0.00113 -> 0.00070: 1.6112x smaller

### go ###
Min: 0.526714 -> 0.544428: 1.03x slower
Avg: 0.529649 -> 0.547626: 1.03x slower
Significant (t=-93.32)
Stddev: 0.00136 -> 0.00136: 1.0028x smaller

### iterative_count ###
Min: 0.109748 -> 0.116513: 1.06x slower
Avg: 0.109816 -> 0.117202: 1.07x slower
Significant (t=-357.08)
Stddev: 0.00008 -> 0.00019: 2.3664x larger

### json_dump_v2 ###
Min: 2.554462 -> 2.609141: 1.02x slower
Avg: 2.564472 -> 2.620013: 1.02x slower
Significant (t=-76.93)
Stddev: 0.00538 -> 0.00481: 1.1194x smaller

### meteor_contest ###
Min: 0.196336 -> 0.191925: 1.02x faster
Avg: 0.196878 -> 0.192698: 1.02x faster
Significant (t=61.86)
Stddev: 0.00053 -> 0.00041: 1.2925x smaller

### nbody ###
Min: 0.228039 -> 0.235551: 1.03x slower
Avg: 0.228857 -> 0.236052: 1.03x slower
Significant (t=-54.15)
Stddev: 0.00130 -> 0.00029: 4.4810x smaller

### pathlib ###
Min: 0.108501 -> 0.105339: 1.03x faster
Avg: 0.109084 -> 0.105619: 1.03x faster
Significant (t=311.08)
Stddev: 0.00022 -> 0.00011: 1.9314x smaller

### regex_effbot ###
Min: 0.057905 -> 0.056447: 1.03x faster
Avg: 0.058055 -> 0.056760: 1.02x faster
Significant (t=79.22)
Stddev: 0.00006 -> 0.00015: 2.7741x larger

### silent_logging ###
Min: 0.070810 -> 0.072436: 1.02x slower
Avg: 0.070899 -> 0.072609: 1.02x slower
Significant (t=-191.59)
Stddev: 0.00004 -> 0.00008: 2.2640x larger

### spectral_norm ###
Min: 0.290255 -> 0.299286: 1.03x slower
Avg: 0.290335 -> 0.299541: 1.03x slower
Significant (t=-572.10)
Stddev: 0.00005 -> 0.00015: 2.8547x larger

### threaded_count ###
Min: 0.107215 -> 0.115206: 1.07x slower
Avg: 0.107488 -> 0.115996: 1.08x slower
Significant (t=-109.39)
Stddev: 0.00016 -> 0.00076: 4.8665x larger

The following not significant results are hidden, use -v to show them:
call_method, call_method_unknown, chaos, fastpickle, fastunpickle, float, formatted_logging, hexiom2, json_load, normal_startup, nqueens, pidigits, raytrace, regex_compile, regex_v8, richards, simple_logging, startup_nosite, telco, unpack_sequence.

대안 제안

__getattribute_super__

이 PEP의 이전 버전에서는 클래스에 다음 정적 메서드를 사용했습니다.:

def __getattribute_super__(cls, name, object, owner): pass

이 메서드는 이름 조회와 디스크립터 호출을 모두 수행했으며, super.__getattribute__와 함께 작동하는 경우로만 제한될 수밖에 없었습니다.

tp_getattro 재사용

새로운 슬롯을 추가하지 않으면 API를 더 단순하고 이해하기 쉽게 유지할 수 있으므로 좋을 것입니다. Issue 18181에 달린 댓글에서는 tp_getattro 슬롯을 재사용하는 방안, 즉 super가 MRO를 따라 모든 메서드의 tp_getattro 슬롯을 호출할 수 있는 방안에 관해 질문했습니다.

이는 작동하지 않습니다. tp_getattro는 MRO의 클래스를 사용해 속성을 확인하기 전에 인스턴스 __dict__를 확인하기 때문입니다. 이는 클래스 딕셔너리를 엿보는 대신 tp_getattro를 사용하면 super class의 의미 체계가 변경된다는 것을 의미합니다.

새 메서드의 다른 배치

이 PEP는 __getdescriptor__를 메타클래스의 메서드로 추가할 것을 제안합니다. 대안으로는 클래스 자체의 클래스 메서드로 추가하는 방법이 있습니다 (__new__가 메타클래스의 메서드가 아니라 클래스의 staticmethod인 것과 유사합니다).

메타클래스의 메서드를 사용하는 것의 이점은 MRO 상의 두 클래스가 서로 다른 메타클래스를 가지고 있어 __getdescriptor__에 대해 다른 동작을 할 수 있는 경우 오류를 발생시킨다는 것입니다. 일반적인 클래스 메서드를 사용하면 이 문제가 감지되지 않고 지나가면서도, 코드 실행 시 미묘한 오류를 일으킬 수 있습니다.

역사

  • 2015년 7월 23일: Guido와 논의한 후 타입 플래그 Py_TPFLAGS_GETDESCRIPTOR를 추가했습니다.

    이 새 플래그는 주로 이전 버전의 CPython용 확장을 로드할 때 충돌을 피하는 데 유용하며, 속도 면에서도 긍정적인 영향을 미칠 수 있습니다.

  • 2014년 7월: 슬롯 이름을 __getdescriptor__로 변경했습니다. 이전 이름은 다른 슬롯들의 명명 스타일과 맞지 않았고 설명력도 떨어졌습니다.

토론 스레드

참고 문헌

  • Issue 18181에는 오래된 프로토타입 구현이 포함되어 있습니다.