Vector类第3版:动态存取属性Vector2d 变成 Vector 之后,就没办法通过名称访问向量的分量了(如 v.x 和 v.y)。现在我们处理的向量可能有大量分量。不过,若能通过单个字母访问前几个分量的话会比较方便。比如,用 x、y 和 z 代替 v[0]、v[1] 和 v[2]。
我们想额外提供下述句法,用于读取向量的前四个分量:
>>> v = Vector(range(10)) >>> v.x 0.0 >>> v.y, v.z, v.t (1.0, 2.0, 3.0)
在 Vector2d 中,我们使用 @property 装饰器把 x 和 y 标记为只读特性(见示例 9-7)。我们可以在 Vector 中编写四个特性,但这样太麻烦。特殊方法 __getattr__ 提供了更好的方式。
属性查找失败后,解释器会调用 __getattr__ 方法。简单来说,对 my_obj.x 表达式,Python 会检查 my_obj 实例有没有名为 x 的属性;如果没有,到类(my_obj.__class__)中查找;如果还没有,顺着继承树继续查找。4 如果依旧找不到,调用 my_obj 所属类中定义的 __getattr__ 方法,传入 self 和属性名称的字符串形式(如 'x')。
4属性查找机制比这复杂得多,复杂的细节在第六部分讲解。目前知道这种简单的说明即可。
示例 10-8 中列出的是我们为 Vector 类定义的 __getattr__ 方法。这个方法的作用很简单,它检查所查找的属性是不是 xyzt 中的某个字母,如果是,那么返回对应的分量。
示例 10-8 vector_v3.py 的部分代码:在 vector_v2.py 中定义的
Vector类里添加__getattr__方法
shortcut_names = 'xyzt'
def __getattr__(self, name):
cls = type(self) ➊
if len(name) == 1: ➋
pos = cls.shortcut_names.find(name) ➌
if 0 <= pos < len(self._components): ➍
return self._components[pos]
msg = '{.__name__!r} object has no attribute {!r}' ➎
raise AttributeError(msg.format(cls, name))
❶ 获取 Vector,后面待用。
❷ 如果属性名只有一个字母,可能是 shortcut_names 中的一个。
❸ 查找那个字母的位置;str.find 还会定位 'yz',但是我们不需要,因此在前一行做了测试。
❹ 如果位置落在范围内,返回数组中对应的元素。
❺ 如果测试都失败了,抛出 AttributeError,并指明标准的消息文本。
__getattr__ 方法的实现不难,但是这样实现还不够。看看示例 10-9 中古怪的交互行为。
示例 10-9 不恰当的行为:为
v.x赋值没有抛出错误,但是前后矛盾
>>> v = Vector(range(5)) >>> v Vector([0.0, 1.0, 2.0, 3.0, 4.0]) >>> v.x # ➊ 0.0 >>> v.x = 10 # ➋ >>> v.x # ➌ 10 >>> v Vector([0.0, 1.0, 2.0, 3.0, 4.0]) # ➍
❶ 使用 v.x 获取第一个元素(v[0])。
❷ 为 v.x 赋新值。这个操作应该抛出异常。
❸ 读取 v.x,得到的是新值,10。
❹ 可是,向量的分量没变。
你能解释为什么会这样吗?具体而言,如果向量的分量数组中没有新值,为什么 v.x 返回 10 ?如果你不能立即给出解释,再看看示例 10-8 前面对 __getattr__ 方法的说明。原因不是很明显,但却是理解本书后面内容的重要基础。
示例 10-9 之所以前后矛盾,是 __getattr__ 的运作方式导致的:仅当对象没有指定名称的属性时,Python 才会调用那个方法,这是一种后备机制。可是,像 v.x = 10 这样赋值之后,v 对象有 x 属性了,因此使用 v.x 获取 x 属性的值时不会调用 __getattr__ 方法了,解释器直接返回绑定到 v.x 上的值,即 10。另一方面,__getattr__ 方法的实现没有考虑到 self._components 之外的实例属性,而是从这个属性中获取 shortcut_names 中所列的“虚拟属性”。
为了避免这种前后矛盾的现象,我们要改写 Vector 类中设置属性的逻辑。
回想第 9 章的最后一个 Vector2d 示例中,如果为 .x 或 .y 实例属性赋值,会抛出 AttributeError。为了避免歧义,在 Vector 类中,如果为名称是单个小写字母的属性赋值,我们也想抛出那个异常。为此,我们要实现 __setattr__ 方法,如示例 10-10 所示。
示例 10-10 vector_v3.py 的部分代码:在
Vector类中实现__setattr__方法
def __setattr__(self, name, value):
cls = type(self)
if len(name) == 1: ➊
if name in cls.shortcut_names: ➋
error = 'readonly attribute {attr_name!r}'
elif name.islower(): ➌
error = "can't set attributes 'a' to 'z' in {cls_name!r}"
else:
error = '' ➍
if error: ➎
msg = error.format(cls_name=cls.__name__, attr_name=name)
raise AttributeError(msg)
super().__setattr__(name, value) ➏
❶ 特别处理名称是单个字符的属性。
❷ 如果 name 是 xyzt 中的一个,设置特殊的错误消息。
❸ 如果 name 是小写字母,为所有小写字母设置一个错误消息。
❹ 否则,把错误消息设为空字符串。
❺ 如果有错误消息,抛出 AttributeError。
❻ 默认情况:在超类上调用 __setattr__ 方法,提供标准行为。
![]()
super()函数用于动态访问超类的方法,对 Python 这样支持多重继承的动态语言来说,必须能这么做。程序员经常使用这个函数把子类方法的某些任务委托给超类中适当的方法,如示例 10-10 所示。12.2 节会进一步探讨super()函数。
为了给 AttributeError 选择错误消息,我查看了内置的 complex 类型的行为,因为 complex 对象是不可变的,而且有一对数据属性:real 和 imag。如果试图修改任何一个属性,complex 实例会抛出 AttributeError,而且把错误消息设为"can't set attribute"。而如果尝试为受特性保护的只读属性赋值(像 9.6 节那样做),得到的错误消息是"readonly attribute"。在 __setitem__ 方法中为 error 字符串选词时,我参考了这两个错误消息,而且更为明确地指出了禁止赋值的属性。
注意,我们没有禁止为全部属性赋值,只是禁止为单个小写字母属性赋值,以防与只读属性 x、y、z 和 t 混淆。
我们知道,在类中声明
__slots__属性可以防止设置新实例属性;因此,你可能想使用这个功能,而不像这里所做的,实现__setattr__方法。可是,正如 9.8.1 节所指出的,不建议只为了避免创建实例属性而使用__slots__属性。__slots__属性只应该用于节省内存,而且仅当内存严重不足时才应该这么做。
虽然这个示例不支持为 Vector 分量赋值,但是有一个问题要特别注意:多数时候,如果实现了 __getattr__ 方法,那么也要定义 __setattr__ 方法,以防对象的行为不一致。
如果想允许修改分量,可以使用 __setitem__ 方法,支持 v[0] = 1.1 这样的赋值,以及(或者)实现 __setattr__ 方法,支持 v.x = 1.1 这样的赋值。不过,我们要保持 Vector 是不可变的,因为在下一节中,我们将把它变成可散列的。