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_Lookup 和 super.__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,并且还完全重新实现了 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 中的类桥:类桥是一个 Python 对象(类),它表示一个 Objective-C 类,并且概念上在 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 中具有相同问题的其他一些函数没有更新名称,因为它们是该文件私有的。
当类具有重写 __getdescriptor__ 的元类时,Objects/typeobject.c 中的属性查找缓存将被禁用,因为对于此类类,使用缓存可能无效。
此 PEP 对自省的影响
使用此 PEP 中引入的方法可能会影响具有使用自定义 __getdescriptor__ 方法的元类的类的自省。本节列出了这些更改。
以下列出的项目仅受自定义 __getdescriptor__ 方法的影响,object 的默认实现不会导致问题,因为它仍然只使用类 __dict__,并且不会导致 object.__getattribute__ 的可见行为发生可见更改。
dir可能不会显示所有属性与自定义
__getattribute__方法一样,当使用__getdescriptor__()方法动态解析属性时,dir() 可能无法看到所有(实例)属性。解决方案很简单:如果类想全面支持内置的 dir() 函数,则使用
__getdescriptor__的类也应实现 __dir__()。inspect.getattr_static可能不会显示所有属性函数
inspect.getattr_static特意不调用__getattribute__和描述符,以避免在使用此函数进行内省时调用用户代码。__getdescriptor__方法也将被忽略,这是inspect.getattr_static的结果可能与builtin.getattr的结果不同的另一种方式。inspect.getmembers和inspect.classify_class_attrs这两个函数都直接访问 MRO 沿线的类的 __dict__,因此可能受自定义
__getdescriptor__方法的影响。具有自定义
__getdescriptor__方法的代码,如果希望与这些方法很好地配合,还需要确保当 Python 代码直接访问__dict__时,其设置是正确的。请注意,
inspect.getmembers由pydoc使用,因此这可能会影响运行时文档自省。- 直接自省类
__dict__任何直接访问类
__dict__进行自省的代码都可能受到自定义__getdescriptor__方法的影响,请参阅上一项。
性能影响
警告:本节中的基准测试结果已过时,待我将补丁移植到当前主干后将更新。我预计本节的结果不会发生重大变化。
微基准测试
Issue 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 更简单易懂。Issue 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__,旧名称与其他槽的命名风格不匹配,且描述性不足。
讨论串
- PEP 的初始版本通过 Message-ID mailto:75030FAC-6918-4E94-95DA-67A88D53E6F5@mac.com 发送
- 更多讨论始于 Message-ID mailto:5BB87CC4-F31B-4213-AAAC-0C0CE738460C@mac.com 的消息
- 更多讨论始于 Message-ID mailto:00AA7433-C853-4101-9718-060468EBAC54@mac.com 的消息
参考资料
- Issue 18181 包含一个过时的原型实现
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0447.rst
最后修改: 2025-02-01 08:55:40 GMT