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 中,将检测到适当形式的方法并将其转换为对象(就像未绑定方法对象一样)。这些对象以 XXX 为名称存储在类的 __dict__ 中。原始方法以其原始名称作为未绑定方法存储。
如果实例中存在任何属性访问处理程序,则会设置一个标志。我们暂时称之为“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