Vector类第1版:与Vector2d类兼容Vector 类的第 1 版要尽量与前一章定义的 Vector2d 类兼容。
然而我们会故意不让 Vector 的构造方法与 Vector2d 的构造方法兼容。为了编写 Vector(3, 4) 和 Vector(3, 4, 5) 这样的代码,我们可以让 __init__ 方法接受任意个参数(通过 *args);但是,序列类型的构造方法最好接受可迭代的对象为参数,因为所有内置的序列类型都是这样做的。示例 10-1 展示了 Vector 类的几种实例化方式。
示例 10-1 测试
Vector.__init__和Vector.__repr__方法
>>> Vector([3.1, 4.2]) Vector([3.1, 4.2]) >>> Vector((3, 4, 5)) Vector([3.0, 4.0, 5.0]) >>> Vector(range(10)) Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
除了新构造方法的签名外,我还确保了传入两个分量(如 Vector([3, 4]))时,Vector2d 类(如 Vector2d(3, 4))的每个测试都能通过,而且得到相同的结果。
如果
Vector实例的分量超过 6 个,repr()生成的字符串就会使用...省略一部分,如示例 10-1 中的最后一行所示。包含大量元素的集合类型一定要这么做,因为字符串表示形式是用于调试的(因此不想让大型对象在控制台或日志中输出几千行内容)。使用reprlib模块可以生成长度有限的表示形式,如示例 10-2 所示。在 Python 2 中,
reprlib模块的名字是repr。2to3工具能自动重写repr导入的内容。
示例 10-2 是第 1 版 Vector 类的实现代码(以示例 9-2 和示例 9-3 中的代码为基础)。
示例 10-2 vector_v1.py:从 vector2d_v1.py 衍生而来
from array import array
import reprlib
import math
class Vector:
typecode = 'd'
def __init__(self, components):
self._components = array(self.typecode, components) ➊
def __iter__(self):
return iter(self._components) ➋
def __repr__(self):
components = reprlib.repr(self._components) ➌
components = components[components.find('['):-1] ➍
return 'Vector({})'.format(components)
def __str__(self):
return str(tuple(self))
def __bytes__(self):
return (bytes([ord(self.typecode)]) +
bytes(self._components)) ➎
def __eq__(self, other):
return tuple(self) == tuple(other)
def __abs__(self):
return math.sqrt(sum(x * x for x in self)) ➏
def __bool__(self):
return bool(abs(self))
@classmethod
def frombytes(cls, octets):
typecode = chr(octets[0])
memv = memoryview(octets[1:]).cast(typecode)
return cls(memv) ➐
❶ self._components 是“受保护的”实例属性,把 Vector 的分量保存在一个数组中。
❷ 为了迭代,我们使用 self._components 构建一个迭代器。1
1iter() 函数和 __iter__ 方法在第 14 章讨论。
❸ 使用 reprlib.repr() 函数获取 self._components 的有限长度表示形式(如 array('d', [0.0, 1.0, 2.0, 3.0, 4.0, ...]))。
❹ 把字符串插入 Vector 的构造方法调用之前,去掉前面的 array('d' 和后面的 )。
❺ 直接使用 self._components 构建 bytes 对象。
❻ 不能使用 hypot 方法了,因此我们先计算各分量的平方之和,然后再使用 sqrt 方法开平方。
❼ 我们只需在 Vector2d.frombytes 方法的基础上改动最后一行:直接把 memoryview 传给构造方法,不用像前面那样使用 * 拆包。
我使用 reprlib.repr 的方式需要做些说明。这个函数用于生成大型结构或递归结构的安全表示形式,它会限制输出字符串的长度,用 '...' 表示截断的部分。我希望 Vector 实例的表示形式是 Vector([3.0, 4.0, 5.0]) 这样,而不是 Vector(array('d', [3.0, 4.0, 5.0])),因为 Vector 实例中的数组是实现细节。因为这两种构造方法的调用方式所构建的 Vector 对象是一样的,所以我选择使用更简单的句法,即传入列表参数。
编写 __repr__ 方法时,本可以使用这个表达式生成简化的 components 显示形式:reprlib.repr(list(self._components))。然而,这么做有点浪费,因为要把 self._components 中的每个元素复制到一个列表中,然后使用列表的表示形式。我没有这么做,而是直接把 self._components 传给 reprlib.repr 函数,然后去掉 [] 外面的字符,如示例 10-2 中 __repr__ 方法的第二行所示。
调用
repr()函数的目的是调试,因此绝对不能抛出异常。如果__repr__方法的实现有问题,那么必须处理,尽量输出有用的内容,让用户能够识别目标对象。
注意,__str__、__eq__ 和 __bool__ 方法与 Vector2d 类中的一样,而 frombytes 方法也只变了一个字符(最后一行把 * 去掉了)。这是 Vector2d 可迭代的好处之一。
顺便说一下,我们本可以让 Vector 继承 Vector2d,但是我没这么做,原因有二。其一,两个构造方法不兼容,因此不建议继承。这一点可以通过适当处理 __init__ 方法的参数解决,不过第二个原因更重要:我想把 Vector 类当作单独的示例,以此实现序列协议。接下来,我们先讨论协议这个术语,然后实现序列协议。