Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 447 – 在元类中添加 __getdescriptor__ 方法

作者:
Ronald Oussoren <ronaldoussoren at mac.com>
状态:
已延期
类型:
标准跟踪
创建:
2013年6月12日
发布历史:
2013年7月2日、2013年7月15日、2013年7月29日、2015年7月22日

目录

摘要

目前 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.__getattribute__ 无条件地查看类 __dict__),这对于可以按需增长新方法的动态类(例如动态代理类)来说可能存在问题。

__getdescriptor__ 方法使得即使使用 超类 查找属性,也可以动态添加属性。

新方法也影响 object.__getattribute__(以及 PyObject_GenericGetAttr),以保持一致性,并在一个地方实现类的动态属性解析。

背景

super.__getattribute__ 的当前行为会导致以下问题:对于充当其他(非 Python)类或类型的动态代理的类,例如 PyObjC。PyObjC 为 Objective-C 运行时中的每个类创建一个 Python 类,并在使用这些方法时在 Objective-C 运行时中查找方法。这对于正常访问来说工作得很好,但对于使用 super 对象进行访问时则无法正常工作。因此,PyObjC 目前包含一个自定义的 super,该 super 必须与其类一起使用,并且还完全重新实现了 PyObject_GenericGetAttr 以进行正常的属性访问。

此 PEP 中的 API 使得可以移除自定义的 super 并简化实现,因为自定义查找行为可以在中心位置添加。

注意

PyObjC 无法预先计算类 __dict__ 的内容,因为 Objective-C 类可以在运行时增长新方法。此外,Objective-C 类往往包含大量方法,而大多数 Python 代码只会使用其中的一小部分,这使得预先计算变得不必要地昂贵。

超类属性查找钩子

super.__getattribute__object.__getattribute__(或 PyObject_GenericGetAttr,尤其是在 C 代码中的 _PyType_Lookup)都会遍历对象的 MRO,并目前查看类的 __dict__ 以查找属性。

通过此提案,这两个查找方法不再查看类的 __dict__,而是调用特殊方法 __getdescriptor__,该方法是在元类上定义的一个槽。该方法的默认实现会在类 __dict__ 中查找名称,这意味着除非元类型实际定义了新的特殊方法,否则属性查找不会改变。

旁白:Python 中的属性解析算法

object.__getattribute__(或 CPython 实现中的 PyObject_GenericGetAttr)实现的属性解析过程相当简单,但在不阅读 C 代码的情况下并非完全如此。

object.__getattribute__ 的当前 CPython 实现基本上等效于以下(伪)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 代理 进行访问。

请注意,特定类可以通过实现自己的 __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 前文所述,此功能更现实的用例是 __getdescriptor__ 方法,该方法根据属性访问动态填充类 __dict__,主要是在无法可靠地使类字典与其源保持同步时,例如因为用于填充 __dict__ 的源也是动态的,并且没有可用于检测对该源的更改的触发器。

PyObjC 中的类桥就是一个例子:类桥是一个表示 Objective-C 类的 Python 对象(类),从概念上讲,它为 Objective-C 类中的每个 Objective-C 方法都包含一个 Python 方法。与 Python 一样,可以向 Objective-C 类添加新方法或替换现有方法,并且没有可用于检测此操作的回调。

在 C 代码中

一个新的类型标志 Py_TPFLAGS_GETDESCRIPTOR,其值为 (1UL << 11),指示存在新的槽并应使用。

一个新的槽 tp_getdescriptor 被添加到 PyTypeObject 结构中,此槽对应于 type 上的 __getdescriptor__ 方法。

该槽具有以下原型

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

此方法应该在 cls 的命名空间中查找 name,而不查看超类,并且不应调用描述符。当找不到 name 时,该方法会返回 NULL 且不设置异常,否则返回一个新引用(而不是借用的引用)。

具有 tp_getdescriptor 槽的类必须将 Py_TPFLAGS_GETDESCRIPTOR 添加到 tp_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__ 方法一样,当使用 __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__ 方法并希望与这些方法良好配合的代码也需要确保当 Python 代码直接访问 __dict__ 时,它被正确设置。

    请注意,inspect.getmemberspydoc 使用,因此这可能会影响运行时文档自省。

  • __dict__ 的直接自省

    任何直接访问类 __dict__ 以进行自省的代码都可能受到自定义 __getdescriptor__ 方法的影响,请参见上一项。

性能影响

警告:本节中的基准测试结果已过时,将在我把补丁移植到当前主干后更新。我不期望本节的结果发生重大变化。

微基准测试

问题 18181 将微基准测试作为其附件之一(pep447-micro-bench.py),专门测试属性查找的速度,包括直接查找和通过 super 查找。

请注意,当使用自定义的 __getdescriptor__ 方法时,具有深层类层次结构的属性查找速度会显著变慢。这是因为当存在此方法时,CPython 的属性查找缓存无法使用。

Pybench

下面的 pybench 输出比较了此 PEP 的实现与常规源代码树,两者均基于更改集 a5681f50bae2,在一台空闲机器和运行 Centos 6.4 的 Core i7 处理器上运行。

即使机器处于空闲状态,运行之间也存在明显的差异,我看到“最短时间”的差异从 -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 更简单易懂。 问题 18181 上的一条评论询问了重用 tp_getattro 插槽的可能性,即 super 可以调用 MRO 沿线所有方法的 tp_getattro 插槽。

这行不通,因为 tp_getattro 会在尝试使用 MRO 中的类解析属性之前查看实例 __dict__。这意味着使用 tp_getattro 而不是查看类字典会改变 超类 的语义。

新方法的替代放置位置

此 PEP 建议在元类上添加 __getdescriptor__ 作为方法。另一种方法是在类本身上将其添加为类方法(类似于 __new__ 如何是类的 静态方法 而不是元类的方法)。

在元类上使用方法的优点是,当 MRO 上的两个类具有不同的元类并且可能对 __getdescriptor__ 具有不同的行为时,它会引发错误。对于普通的类方法,这个问题将无法被检测到,同时它可能会在运行代码时导致细微的错误。

历史

  • 2015 年 7 月 23 日:在与 Guido 讨论后添加了类型标志 Py_TPFLAGS_GETDESCRIPTOR

    新标志主要用于避免在加载旧版 CPython 的扩展时崩溃,并且也可能对速度产生积极影响。

  • 2014 年 7 月:将插槽重命名为 __getdescriptor__,旧名称与其他插槽的命名风格不匹配,并且描述性较差。

讨论线程

参考文献


来源:https://github.com/python/peps/blob/main/peps/pep-0447.rst

上次修改:2023-09-09 17:39:29 GMT