- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- Python 术语表
- Python 版本表
- 排版约定
- 使用代码示例
- 第一部分 序幕
- 第 1 章 Python 数据模型
- 第二部分 数据结构
- 第 2 章 序列构成的数组
- 第 3 章 字典和集合
- 第 4 章 文本和字节序列
- 第三部分 把函数视作对象
- 第 5 章 一等函数
- 第 6 章 使用一等函数实现设计模式
- 第 7 章 函数装饰器和闭包
- 第四部分 面向对象惯用法
- 第 8 章 对象引用、可变性和垃圾回收
- 第 9 章 符合 Python 风格的对象
- 第 10 章 序列的修改、散列和切片
- 第 11 章 接口:从协议到抽象基类
- 第 12 章 继承的优缺点
- 第 13 章 正确重载运算符
- 第五部分 控制流程
- 第 14 章 可迭代的对象、迭代器和生成器
- 14.1 Sentence 类第1版:单词序列
- 14.2 可迭代的对象与迭代器的对比
- 14.3 Sentence 类第2版:典型的迭代器
- 14.4 Sentence 类第3版:生成器函数
- 14.5 Sentence 类第4版:惰性实现
- 14.6 Sentence 类第5版:生成器表达式
- 14.7 何时使用生成器表达式
- 14.8 另一个示例:等差数列生成器
- 14.9 标准库中的生成器函数
- 14.10 Python 3.3 中新出现的句法:yield from
- 14.11 可迭代的归约函数
- 14.12 深入分析 iter 函数
- 14.13 案例分析:在数据库转换工具中使用生成器
- 14.14 把生成器当成协程
- 14.15 本章小结
- 14.16 延伸阅读
- 第 15 章 上下文管理器和 else 块
- 第 16 章 协程
- 第 17 章 使用期物处理并发
- 第 18 章 使用 asyncio 包处理并发
- 第六部分 元编程
- 第 19 章 动态属性和特性
- 第 20 章 属性描述符
- 第 21 章 类元编程
- 结语
- 延伸阅读
- 附录 A 辅助脚本
- Python 术语表
- 作者简介
- 关于封面
10.4 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.1 切片原理
一例胜千言,我们来看看示例 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。研究切片对象时,我在 Python 控制台中执行了 dir() 和 help(),这才发现 slice.indices() 方法。这也表明交互式控制台是个有价值的工具,能发现新事物。
2现在已经有了,参见:https://docs.python.org/3/reference/datamodel.html?highlight=indices#slice.indices。——编者注
在 Vector 类中无需使用 slice.indices() 方法,因为收到切片参数时,我们会委托 _components 数组处理。但是,如果你没有底层序列类型作为依靠,那么使用这个方法能节省大量时间。
现在我们知道如何处理切片了,下面来看 Vector.__getitem__ 方法改进后的实现。
10.4.2 能处理切片的__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 不支持多维索引,因此索引元组或多个切片会抛出错误。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论