Vector类第2版:可切片的序列如 FrenchDeck 类所示,如果能委托给对象中的序列属性(如 self._components 数组),支持序列协议特别简单。下述只有一行代码的 __len__ 和 __getitem__ 方法是个好的开始:
class Vector:
# 省略了很多行
# ...
def __len__(self):
return len(self._components)
def __getitem__(self, index):
return self._components[index]
添加这两个方法之后,就能执行下述操作了:
>>> v1 = Vector([3, 4, 5])
>>> len(v1)
3
>>> v1[0], v1[-1]
(3.0, 5.0)
>>> v7 = Vector(range(7))
>>> v7[1:4]
array('d', [1.0, 2.0, 3.0])
可以看到,现在连切片都支持了,不过尚不完美。如果 Vector 实例的切片也是 Vector 实例,而不是数组,那就更好了。前面那个 FrenchDeck 类也有类似的问题:切片得到的是列表。对 Vector 来说,如果切片生成普通的数组,将会缺失大量功能。
想想内置的序列类型,切片得到的都是各自类型的新实例,而不是其他类型。
为了把 Vector 实例的切片也变成 Vector 实例,我们不能简单地委托给数组切片。我们要分析传给 __getitem__ 方法的参数,做适当的处理。
下面来看 Python 如何把 my_seq[1:3] 句法变成传给 my_seq.__getitem__(...) 的参数。
一例胜千言,我们来看看示例 10-4。
示例 10-4 了解
__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))
❶ 在这个示例中,__getitem__ 直接返回传给它的值。
❷ 单个索引,没什么新奇的。
❸ 1:4 表示法变成了 slice(1, 4, None)。
❹ slice(1, 4, 2) 的意思是从 1 开始,到 4 结束,步幅为 2。
❺ 神奇的事发生了:如果 [] 中有逗号,那么 __getitem__ 收到的是元组。
❻ 元组中甚至可以有多个切片对象。
现在,我们来仔细看看 slice 本身,如示例 10-5 所示。
示例 10-5 查看
slice类的属性
>>> slice # ➊ <class 'slice'> >>> dir(slice) # ➋ ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'indices', 'start', 'step', 'stop']
❶ slice 是内置的类型(2.4.2 节首次出现)。
❷ 通过审查 slice,发现它有 start、stop 和 step 数据属性,以及 indices 方法。
在示例 10-5 中,调用 dir(slice) 得到的结果中有个 indices 属性,这个方法有很大的作用,但是鲜为人知。help(slice.indices) 给出的信息如下。
S.indices(len) -> (start, stop, stride)
给定长度为 len 的序列,计算 S 表示的扩展切片的起始(start)和结尾(stop)索引,以及步幅(stride)。超出边界的索引会被截掉,这与常规切片的处理方式一样。
换句话说,indices 方法开放了内置序列实现的棘手逻辑,用于优雅地处理缺失索引和负数索引,以及长度超过目标序列的切片。这个方法会“整顿”元组,把 start、stop 和 stride 都变成非负数,而且都落在指定长度序列的边界内。
下面举几个例子。假设有个长度为 5 的序列,例如 'ABCDE':
>>> slice(None, 10, 2).indices(5) # ➊ (0, 5, 2) >>> slice(-3, None, None).indices(5) # ➋ (2, 5, 1)
❶ 'ABCDE'[:10:2] 等同于 'ABCDE'[0:5:2]
❷ 'ABCDE'[-3:] 等同于 'ABCDE'[2:5:1]
写作本书时,在线版 Python 库参考好像还没有
slice.indices方法的文档。2Python Python/C API 参考手册中有类似的 C 语言函数的文档, PySlice_GetIndicesEx(https://docs.python.org/3/c-api/slice.html#c.PySlice_GetIndicesEx)。研究切片对象时,我在 Python 控制台中执行了dir()和help(),这才发现slice.indices()方法。这也表明交互式控制台是个有价值的工具,能发现新事物。
2现在已经有了,参见:https://docs.python.org/3/reference/datamodel.html?highlight=indices#slice.indices。——编者注
在 Vector 类中无需使用 slice.indices() 方法,因为收到切片参数时,我们会委托 _components 数组处理。但是,如果你没有底层序列类型作为依靠,那么使用这个方法能节省大量时间。
现在我们知道如何处理切片了,下面来看 Vector.__getitem__ 方法改进后的实现。
__getitem__方法示例 10-6 列出了让 Vector 表现为序列所需的两个方法:__len__ 和 __getitem__ (后者现在能正确地处理切片了)。
示例 10-6 vector_v2.py 的部分代码:为 vector_v1.py 中的
Vector类(见示例 10-2)添加__len__和__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
3必须在 vector_v2.py 的开头加上 import numbers。——编者注
❺ ……那就返回 _components 中相应的元素。
❻ 否则,抛出异常。
大量使用
isinstance可能表明面向对象设计得不好,不过在__getitem__方法中使用它处理切片是合理的。注意,示例 10-6 中测试时用的是numbers.Integral,这是一个抽象基类(Abstract Base Class,ABC)。在isinstance中使用抽象基类做测试能让 API 更灵活且更容易更新,原因参见第 11 章。可惜,Python 3.4 的标准库中没有slice的抽象基类。
为了确定在 __getitem__ 的 else 子句中会抛出哪个异常,我在交互式控制台中查看了 'ABC'[1, 2] 的结果。我发现,Python 抛出的是 TypeError;我还从错误消息中复制了表述方式,“indices must be integers”。为了创建符合 Python 风格的对象,我们要模仿 Python 内置的对象。
把示例 10-6 中的代码添加到 Vector 类中之后,切片行为就正确了,如示例 10-7 所示。
示例 10-7 测试示例 10-6 中改进的
Vector.__getitem__方法
>>> v7 = Vector(range(7))
>>> v7[-1] ➊
6.0
>>> v7[1:4] ➋
Vector([1.0, 2.0, 3.0])
>>> v7[-1:] ➌
Vector([6.0])
>>> v7[1,2] ➍
Traceback (most recent call last):
...
TypeError: Vector indices must be integers
❶ 单个整数索引只获取一个分量,值为浮点数。
❷ 切片索引创建一个新 Vector 实例。
❸ 长度为 1 的切片也创建一个 Vector 实例。
❹ Vector 不支持多维索引,因此索引元组或多个切片会抛出错误。