- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- 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 术语表
- 作者简介
- 关于封面
2.4 切片
在 Python 里,像列表(list)、元组(tuple)和字符串(str)这类序列类型都支持切片操作,但是实际上切片操作比人们所想象的要强大很多。
这一节主要讨论的是这些高级切片形式的用法,它们的实现方法则会在第 10 章的一个自定义类里提到。这么做主要是为了符合这本书的哲学:先讲用法,第四部分中再来讲如何创建新类。
2.4.1 为什么切片和区间会忽略最后一个元素
在切片和区间操作里不包含区间范围的最后一个元素是 Python 的风格,这个习惯符合 Python、C 和其他语言里以 0 作为起始下标的传统。这样做带来的好处如下。
当只有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素:range(3) 和 my_list[:3] 都返回 3 个元素。
当起止位置信息都可见时,我们可以快速计算出切片和区间的长度,用后一个数减去第一个下标(stop - start)即可。
这样做也让我们可以利用任意一个下标来把序列分割成不重叠的两部分,只要写成 my_list[:x] 和 my_list[x:] 就可以了,如下所示。
>>> l = [10, 20, 30, 40, 50, 60] >>> l[:2] # 在下标2的地方分割 [10, 20] >>> l[2:] [30, 40, 50, 60] >>> l[:3] # 在下标3的地方分割 [10, 20, 30] >>> l[3:] [40, 50, 60]
计算机科学家 Edsger W. Dijkstra 对这一风格的解释应该是最好的,详见“延伸阅读”中给出的最后一个参考资料。
接下来进一步看看 Python 解释器是如何理解切片操作的。
2.4.2 对对象进行切片
一个众所周知的秘密是,我们还可以用 s[a:b:c] 的形式对 s 在 a 和 b 之间以 c 为间隔取值。c 的值还可以为负,负值意味着反向取值。下面的 3 个例子更直观些:
>>> s = 'bicycle' >>> s[::3] 'bye' >>> s[::-1] 'elcycib' >>> s[::-2] 'eccb'
另一个例子是在第 1 章中用 deck[12::13] 的形式在未洗过的牌里把每种花色的 A 拿出来:
>>> deck[12::13] [Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'), Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]
a:b:c 这种用法只能作为索引或者下标用在 [] 中来返回一个切片对象:slice(a, b, c)。在 10.4.1 节中会讲到,对 seq[start:stop:step] 进行求值的时候,Python 会调用 seq.__getitem__(slice(start, stop, step))。就算你还不会自定义序列类型,了解一下切片对象也是有好处的。例如你可以给切片命名,就像电子表格软件里给单元格区域取名字一样。
比如,要解析示例 2-11 中所示的纯文本文件,这时使用有名字的切片比用硬编码的数字区间要方便得多,注意示例里的 for 循环的可读性有多强。
示例 2-11 纯文本文件形式的收据以一行字符串的形式被解析
>>> invoice = """ ... 0.....6................................40........52...55........ ... 1909 Pimoroni PiBrella $17.50 3 $52.50 ... 1489 6mm Tactile Switch x20 $4.95 2 $9.90 ... 1510 Panavise Jr. - PV-201 $28.00 1 $28.00 ... 1601 PiTFT Mini Kit 320x240 $34.95 1 $34.95 ... """ >>> SKU = slice(0, 6) >>> DESCRIPTION = slice(6, 40) >>> UNIT_PRICE = slice(40, 52) >>> QUANTITY = slice(52, 55) >>> ITEM_TOTAL = slice(55, None) >>> line_items = invoice.split('\n')[2:] >>> for item in line_items: ... print(item[UNIT_PRICE], item[DESCRIPTION]) ... $17.50 Pimoroni PiBrella $4.95 6mm Tactile Switch x20 $28.00 Panavise Jr. - PV-201 $34.95 PiTFT Mini Kit 320x240
在 10.4 节还有更多机会来了解切片(slice)对象。如果从 Python 用户的角度出发,切片还有个两个额外的功能:多维切片和省略表示法(...)。
2.4.3 多维切片和省略
[] 运算符里还可以使用以逗号分开的多个索引或者是切片,外部库 NumPy 里就用到了这个特性,二维的 numpy.ndarray 就可以用 a[i, j] 这种形式来获取,抑或是用 a[m:n, k:l] 的方式来得到二维切片。稍后的示例 2-22 会展示这个用法。要正确处理这种 [] 运算符的话,对象的特殊方法 __getitem__ 和 __setitem__ 需要以元组的形式来接收 a[i, j] 中的索引。也就是说,如果要得到 a[i, j] 的值,Python 会调用 a.__getitem__((i, j))。
Python 内置的序列类型都是一维的,因此它们只支持单一的索引,成对出现的索引是没有用的。
省略(ellipsis)的正确书写方法是三个英语句号(...),而不是 Unicdoe 码位 U+2026 表示的半个省略号(...)。省略在 Python 解析器眼里是一个符号,而实际上它是 Ellipsis 对象的别名,而 Ellipsis 对象又是 ellipsis 类的单一实例。2 它可以当作切片规范的一部分,也可以用在函数的参数清单中,比如 f(a, ..., z),或 a[i:...]。在 NumPy 中,... 用作多维数组切片的快捷方式。如果 x 是四维数组,那么 x[i, ...] 就是 x[i, :, :, :] 的缩写。如果想了解更多,请参见“Tentative NumPy Tutorial”。
2是的,你没看错,ellipsis 是类名,全小写,而它的内置实例写作 Ellipsis。这其实跟 bool 是小写,但是它的两个实例写作 True 和 False 异曲同工。
在写这本书的时候,我还没有发现在 Python 的标准库里有任何 Ellipsis 或者是多维索引的用法。如果你知道,请告诉我。这些句法上的特性主要是为了支持用户自定义类或者扩展,比如 NumPy 就是个例子。
除了用来提取序列里的内容,切片还可以用来就地修改可变序列,也就是说修改的时候不需要重新组建序列。
2.4.4 给切片赋值
如果把切片放在赋值语句的左边,或把它作为 del 操作的对象,我们就可以对序列进行嫁接、切除或就地修改操作。通过下面这几个例子,你应该就能体会到这些操作的强大功能:
>>> l = list(range(10)) >>> l [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> l[2:5] = [20, 30] >>> l [0, 1, 20, 30, 5, 6, 7, 8, 9] >>> del l[5:7] >>> l [0, 1, 20, 30, 5, 8, 9] >>> l[3::2] = [11, 22] >>> l [0, 1, 20, 11, 5, 22, 9] >>> l[2:5] = 100 ➊ Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can only assign an iterable >>> l[2:5] = [100] >>> l [0, 1, 100, 22, 9]
➊ 如果赋值的对象是一个切片,那么赋值语句的右侧必须是个可迭代对象。即便只有单独一个值,也要把它转换成可迭代的序列。
序列的拼接操作可谓是众所周知,任何一本 Python 入门教材都会介绍 + 和 * 的用法,但是在这些用法的背后还有一些可能被忽视的细节。下面就来看看这两种操作。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论