PEP 213 – 属性访问处理器
- 作者:
- Paul Prescod <paul at prescod.net>
- 状态:
- 已延期
- 类型:
- 标准轨迹
- 创建:
- 2000年7月21日
- Python 版本:
- 2.1
- 历史记录:
引言
在 Python 代码和扩展模块中,可以(甚至相对常见)“捕获”实例的客户端代码尝试设置属性并执行代码的情况。换句话说,即使底层实现执行的是某些计算而不是直接修改绑定,也可以允许用户使用属性赋值/检索/删除语法。
本 PEP 描述了一个特性,该特性使为 Python 实例实现这些处理器变得更容易、更高效和更安全。
理由
场景 1
您有一个已部署的类,它在一个名为“stdout”的属性上工作。一段时间后,您认为在赋值时最好检查 stdout 是否确实是一个具有“write”方法的对象。您宁愿捕获赋值并检查对象的类型,而不是更改为 setstdout 方法(这与已部署的代码不兼容)。
场景 2
您希望尽可能兼容具有属性赋值概念的对象模型。它可能是 W3C 文档对象模型或特定的 COM 接口(例如 PowerPoint 接口)。在这种情况下,您可能希望模型中的属性显示为 Python 接口中的属性,即使底层实现根本不使用属性。
场景 3
用户希望将属性设为只读。
简而言之,此特性允许程序员出于任何目的将模块的接口与底层实现分离。再次强调,这不是一个新特性,而只是现有约定的新语法。
当前解决方案
使某些属性成为只读
class foo:
def __setattr__( self, name, val ):
if name=="readonlyattr":
raise TypeError
elif name=="readonlyattr2":
raise TypeError
...
else:
self.__dict__["name"]=val
这存在以下问题
- 方法的创建者必须完全了解类层次结构中的其他位置是否也出于任何特定目的捕获了
__setattr__
。如果是,她必须专门调用该方法而不是分配给字典。有很多不同的原因会导致__setattr__
重载,因此存在相当大的冲突可能性。例如,对象数据库实现通常会出于完全不同的目的重载 setattr。 - 基于字符串的 switch 语句强制所有属性处理器都在代码中的一个位置指定。然后它们可以分派到特定于任务的方法(为了模块化),但这可能会导致性能问题。
- 设置、获取和删除的逻辑必须位于
__getattr__
、__setattr__
和__delattr__
中。同样,这可以通过额外的层级方法调用来缓解,但这效率低下。
建议的语法
特殊方法应使用以下形式的声明来声明自身
class x:
def __attr_XXX__(self, op, val ):
if op=="get":
return someComputedValue(self.internal)
elif op=="set":
self.internal=someComputedValue(val)
elif op=="del":
del self.internal
客户端代码如下所示
fooval=x.foo
x.foo=fooval+5
del x.foo
语义
所有三种类型的属性引用都应调用该方法。op 参数可以是“get”/“set”/“del”。当然,此字符串将被驻留,因此对字符串的实际检查将非常快。
不允许在与名为 __attr_XXX__ 的方法相同的实例中实际拥有名为 XXX 的属性。
__attr_XXX__ 的实现优先于基于以下原则的__getattr__
的实现:__getattr__
应该仅在找不到合适的属性后才调用。
__attr_XXX__ 的实现优先于__setattr__
的实现,以保持一致性。然而,相反的选择似乎也相当可行。__del_y__ 也一样。
建议的实现
有一种称为属性访问处理器的新的对象类型。此类型的对象具有以下属性
name (e.g. XXX, not __attr__XXX__)
method (pointer to a method object)
在 PyClass_New 中,将检测适当形式的方法并将其转换为对象(就像未绑定方法对象一样)。这些存储在类__dict__
中,名称为 XXX。原始方法作为未绑定方法存储在其原始名称下。
如果实例中存在任何属性访问处理器,则设置一个标志。现在让我们将其称为“I_have_computed_attributes”。派生类从基类继承该标志。实例从类继承该标志。
获取操作照常进行,直到返回对象之前。除了当前检查返回的对象是否为方法之外,它还会检查返回的对象是否为访问处理器。如果是,它将调用 getter 方法并返回值。要删除属性访问处理器,您可以直接修改字典。
设置操作通过检查“I_have_computed_attributes”标志来进行。如果未设置,则所有操作都按今天的方式进行。如果已设置,则我们必须对请求的对象名称执行字典获取。如果它返回一个属性访问处理器,那么我们使用该值调用 setter 函数。如果它返回任何其他对象,那么我们丢弃结果并继续像今天一样进行。请注意,拥有属性访问处理器将轻微影响特定实例上所有设置的属性“设置”性能,但不会比今天使用__setattr__
更差。获取操作比今天使用__getattr__
更有效。
I_have_computed_attributes 标志旨在消除对不使用此功能的对象的每个“设置”进行额外“获取”操作而导致的性能下降。检查此标志应该对所有对象产生微不足道的性能影响。
删除操作的实现类似于设置操作的实现。
注意事项
- 您可能会注意到,我还没有提出任何逻辑来使 I_have_computed_attributes 标志在将属性添加到实例的字典或从中删除属性时保持最新。这与当前的 Python 保持一致。如果您在对象使用后向其添加
__setattr__
方法,则该方法的行为将与在“编译”时可用时不同。可以说,这种动态性不值得付出额外的实现工作。此代码段演示了当前行为>>> def prn(*args):print args >>> class a: ... __setattr__=prn >>> a().foo=5 (<__main__.a instance at 882890>, 'foo', 5) >>> class b: pass >>> bi=b() >>> bi.__setattr__=prn >>> b.foo=5
- 分配给 __dict__[“XXX”] 可以覆盖 __attr_XXX__ 的属性访问处理器。通常,访问处理器会将信息存储在私有 __XXX 变量中
- 尝试对对象本身调用 setattr 或 getattr 的属性访问处理器可能会导致无限循环(与
__getattr__
一样)。同样,解决方案是使用特殊的(通常是私有的)变量,例如 __XXX。
注意
PEP 252中描述的描述符机制功能强大到足以更直接地支持这一点。“getset”构造函数可能会添加到语言中,从而使这成为可能
class C:
def get_x(self):
return self.__x
def set_x(self, v):
self.__x = v
x = getset(get_x, set_x)
可能会添加额外的语法糖,或者可以识别命名约定。
来源:https://github.com/python/peps/blob/main/peps/pep-0213.rst
上次修改时间:2023-09-09 17:39:29 GMT