1. Vector类要求
- Vector类的第1版和之前的Vector2d类兼容
- 提供切片支持
- 序列协议—— __len__和__getitem__两个方法
- 自定义的格式语言扩展
2. Vector类的实现
这里我们重点实现的功能为:
从类中,任意抽取其中若干元素,可以实现类似列表的功能。
2.1 Vector类的第一版
如果要实现序列协议,时需要实现__len__和__getitem__协议即可
class Vecotr:
def __len__(self):
return len(self._components)
def __getitem__(self, index):
return self._components[index]
这样一来,就可以实现了列表的全部功能,连切片都可以支持,但是存在一个缺陷:
Vector实例的切片并不是Vector类,而是list类
这种缺陷使得切片后的元素不能够实现Vector类内置的功能,也是的定义上不统一,因为向量的部分也应该是向量。那么该如何修改?所以继续观察切片的原理。
Python的协议
在面向对象编程中,协议是非正式的接口,只在文档中定义,在代码中不定义。例如,Python 的序列协议只需要 __len __ 和 __getitem __ 两个方法。任何类(如 Spam),只要使用 标准的签名和语义实现了这两个方法,就能用在任何期待序列的地方。 Spam 是不是哪个类的子类无关紧要,只要提供了所需的方法即可。
2.2 第二版与切片的原理
用一个例子来看,切片到底是怎么完成?首先需要观察,当我们切片时,给__getitem__函数传入了什么参数。
class MySeq:
def __getitem__(self, index):
return index
>>> s = MySeq()
>>> s[1] # ➋
1
>>> s[1:4] # ➌
slice(1, 4, None)
>>> s[1:4:2] # ➍
slice(1, 4, 2)
>>> s[1:4:2, 9] # ➎
(slice(1, 4, 2), 9)
>>> s[1:4:2, 7:9] # ➏
(slice(1, 4, 2), slice(7, 9, None))
从案例中可以看到,切片如数输入的类型时个slice类,slice系Python的内置类。内省slice类中,可以看到一个非常有用的方法:S.indices(len) -> (start, stop, stride),这个方法可把indices转成标准形式。基于此,我们可以改造我们的__getitem__方法。
def __len__(self):
return len(self._components)
def __getitem__(self, index):
cls = type(self) ➊
if isinstance(index, slice): ➋
return cls(self._components[index]) ➌
elif isinstance(index, numbers.Integral): ➍
return self._components[index] ➎
else:
msg = '{cls.__name__} indices must be integers'
raise TypeError(msg.format(cls=cls)) ➏
➊ 获取实例所属的类(即 Vector),供后面使用。
➋ 如果 index 参数的值是 slice 对象……
➌ ……调用类的构造方法,使用 _components 数组的切片构建一个新 Vector 实例。
➍ 如果 index 是 int 或其他整数类型……3
➎ ……那就返回 _components 中相应的元素。
➏ 否则,抛出异常。
这样就可以实现切片的功能,其实在此基础上,还可以实现一些numpy中的一些特性。
2.3 第三版与动态存储特性
Vector2d 变成 Vector 之后,就没办法通过名称访问向量的分量了(如 v.x 和 v.y)。现在我们处理的向量可能有大量分量。不过,若能通过单个字母访问前几个分量的话会比较方便。比如,用 x、 y 和 z 代替 v[0]、 v[1] 和 v[2]。
在 Vector2d 中,我们使用 @property 装饰器把 x 和 y 标记为只读特性。我们可以在 Vector 中编写四个特性,但这样太麻烦。特殊方法 __getattr__提供了更好的方式。
因为Python在查找属性失败后,解释器会调用__getattr__方法。简单来说,对 my_obj.x 表达式, Python会检查 my_obj 实例有没有名为 x 的属性;如果没有到在类( my_obj.class)中查找;如果还没有,顺着继承树继续查找。
对于此,我们将之前类的代码增加方法
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))
但是如此写程序,只是实现了查看,但是并没有实现修改,所以需要再次实现__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__ 方法,提供标准行为。