David Beazley 和 Brian K. Jones 的《Python Cookbook(第 3 版)中文版》一书的第 1 章“数据结构”里有很多专门针对序列的小窍门。尤其是“1.11 对切片命名”这一部分,从中我学会把切片赋值给变量以改善可读性,本书的示例 2-11 对此做了说明。
《Python Cookbook(第 2 版)中文版》用的是 Python 2.4,但其中大部分的代码都可以运行在 Python 3 中。该书第 5 章和第 6 章的大部分内容都是跟序列有关的。该书的编者有 Alex Martelli、Anna Martelli Ravenscroft 和 David Ascher,另外还有十几位 Python 程序员为内容做出了贡献。第 3 版则是从零开始完全重写的,书的重点也放在了 Python 的语义上,特别是 Python 3 带来的那些变化,而不是像第 2 版那样把重点放在如何解决实际问题上。虽然第 2 版中有些内容已经不是最优解了,但是我仍然推荐把这两个版本都读一读。
Python 官方网站中的“Sorting HOW TO”一文(https://docs.python.org/3/howto/sorting.html)通过几个例子讲解了 sorted 和 list.sort 的高级用法。
“PEP 3132 — Extended Iterable Unpacking”(https://www.python.org/dev/peps/pep-3132/)算得上是使用 *extra 句法进行平行赋值的权威指南。如果你想窥探一下 Python 本身的开发过程,“Missing *-unpacking generalizations”(http://bugs.python.org/issue2292)是一个 bug 追踪器,里面有很多关于如何更广泛地使用可迭代对象拆包的讨论和提议。“PEP 448—Additional Unpacking Generalizations”(https://www.python.org/dev/peps/pep-0448/)就是这些讨论的直接结果。就在我写这本书的时候,这些改动也许会被集成在 Python 3.5 中。
Eli Bendersky 的博客文章“Less Copies in Python with the Buffer Protocol and memoryviews”(http://eli.thegreenplace.net/2011/11/28/less-copies-in-python-with-the-buffer-protocol-and-memoryviews/)里有一些关于 memoryview 的小教程。
市面上关于 NumPy 的书多到数不清,其中有些书的名字里都不带“NumPy”这几个字,例如 Wes McKinney 的《利用 Python 进行数据分析》一书。
科学家尤其钟爱 NumPy 和 SciPy 的强大以及与 Python 的交互式控制台的结合,于是他们专门开发了 IPython。IPython 是 Python 自带控制台的强大替代品,而且它还附带了图形界面、内嵌的图表渲染、文学编程支持(代码和文本互动)和 PDF 渲染。而这些互动多媒体对话还能以 IPython 记事本的形式在网络上分享——详见“IPython 记事本”(http://ipython.org/notebook.html)中的截屏和视频。IPython 在 2012 年非常流行,背后的开发者收到了一笔 1 150 000 美元的捐赠。这笔来自 Sloan 基金的捐赠是专门用来支持加州大学伯克利分校的开发者的,好让他们能在 2013—2014 年期间按计划实现 IPython 的扩展。
Python 标准库里的“8.3. collections — Container datatypes”(https://docs.python.org/3/library/collections.html)里有一些关于双向队列和其他集合类型的使用技巧。
Python 里的范围(range)和切片都不会返回第二个下标所指的元素,Edsger W. Dijkstra 在一个很短的备忘录里为这一惯例做了最好的辩护。这篇名为“Why Numbering Should Start at Zero”(http://www.cs.utexas.edu/users/EWD/transcriptions/EWD08xx/EWD831.html)的备忘录其实是关于数学符号的,但是它跟 Python 的关系在于,Dijkstra 教授严肃又活泼地解释了为什么 2,3,…,12 这个序列应该表达为 2 ≤ i < 13。备忘录对其他所有的表达习惯都作出了反驳,同时还说明了为什么不能让用户自行决定表达习惯。虽然文章的标题是关于基于 0 的下标,但是整篇文章其实都在说为什么 'ABCED'[1:3] 的结果应该是 'BC' 而不是 'BCD',以及为什么 2,3,…,12 应该写作 range(2, 13)。(顺便说一下,这份备忘录是手写的,但是字写得干净漂亮。如果有人就此创作 Dijkstra 字体,我应该会买一份。)
杂谈
元组的本质
2012 年,我在 PyCon US 上贴了一张关于 ABC 语言的墙报。Guido 在开创 Python 语言之前曾做过 ABC 解释器方面的工作,因此他也去看了我的墙报。我们聊了不少,而且都提到了 ABC 里的
compounds类型。compounds算得上是 Python 元组的鼻祖,它既支持平行赋值,又可以用在字典(dict)里作为合成键(ABC 里对应字典的类型是表格,即table)。但compounds不属于序列,它不是迭代类型,也不能通过下标来提取某个值,更不用说切片了。要么把compounds对象当作整体来用,要么用平行赋值把里面所有的字段都提取出来,仅此而已。我跟 Guido 说,上面这些限制让
compounds的作用变得很明确,它只能用作没有字段名的记录。Guido 回应说,Python 里的元组能当作序列来使用,其实是一个取巧的实现。这其实体现了 Python 的实用主义,而实用主义是 Python 较之 ABC 更好用也更成功的原因。从一个语言开发人员的角度来看,让元组具有序列的特性可能需要下点功夫,结果则是设计出了一个概念上并不如
compounds纯粹,却更灵活的元组——它甚至能当成不可变的列表来使用。说真的,不可变列表这种数据类型在编程语言里真的非常好用(其实
frozenlist这个名字更酷),而 Python 里这种类型其实就是一个行为很像序列的元组。“优雅是简约之父”
很久以前,
*extra这种语法就在函数里用来把多个元素赋值给一个参数了。(我有本出版于 1996 年的讲 Python 1.4 的书,里面就提到了这个用法。)Python 1.6 或更新的版本里,这个语法在函数定义里用来把一个可迭代对象拆包成不同的参数,这算是跟上面说的那种用法互补。这一设计直观而优雅,并且取代了 Python 里的apply函数。如今到了 Python 3,*extra这个写法又可以用在赋值表达式的左侧,从而在平行赋值里接收多余的元素。这一点让这个本来就很实用的语法锦上添花。像这样的改进一个接着一个,让 Python 变得越来越灵活,越来越统一,也越来越简单。“优雅是简约之父”(“Elegance begets simplicity”)是 2009 年在芝加哥的 PyCon 的口号,印在 PyCon 的 T 恤上,同样印在 T 恤上的还有 Bruce Eckel 画的《易经》第二十二卦,即贲卦的卦象。贲代表着典雅高贵。这也是我最喜欢的一件 PyCon 的 T 恤。
扁平序列和容器序列
为了解释不同序列类型里不同的内存模型,我用了容器序列和扁平序列这两个说法。其中“容器”一词来自“Data Model”文档(https://docs.python.org/3/reference/datamodel.html#objects-values-and-types):
有些对象里包含对其他对象的引用;这些对象称为容器。
因此,我特别使用了“容器序列”这个词,因为 Python 里有是容器但并非序列的类型,比如
dict和set。容器序列可以嵌套着使用,因为容器里的引用可以针对包括自身类型在内的任何类型。与此相反,扁平序列因为只能包含原子数据类型,比如整数、浮点数或字符,所以不能嵌套使用。
称其为“扁平序列”是因为我希望有个名词能够跟“容器序列”形成对比。这个词是我自己发明的,专门用来指代 Python 中“不是容器序列”的序列,在其他地方你可能找不到这样的用法。如果这个词出现在维基百科上面的话,我们需要给它加上“原创研究”标签。我更倾向于把这类词称作“自创名词”,希望它能对你有所帮助并为你所用。
混合类型列表
Python 入门教材往往会强调列表是可以同时容纳不同类型的元素的,但是实际上这样做并没有什么特别的好处。我们之所以用列表来存放东西,是期待在稍后使用它的时候,其中的元素有一些通用的特性(比如,列表里存的是一类可以“呱呱”叫的动物,那么所有的元素都应该会发出这种叫声,即便其中一部分元素类型并不是鸭子)。在 Python 3 中,如果列表里的东西不能比较大小,那么我们就不能对列表进行排序:
>>> l = [28, 14, '28', 5, '9', '1', 0, 6, '23', 19] >>> sorted(l) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unorderable types: str() < int()元组则恰恰相反,它经常用来存放不同类型的的元素。这也符合它的本质,元组就是用作存放彼此之间没有关系的数据的记录。
key 参数很妙
list.sort、sorted、max和min函数的key参数是一个很棒的设计。其他语言里的排序函数需要用户提供一个接收两个参数的比较函数作为参数,像是 Python 2 里的cmp(a, b)。用key参数能把事情变得简单且高效。说它更简单,是因为只需要提供一个单参数函数来提取或者计算一个值作为比较大小的标准即可,而 Python 2 的这种设计则需要用户写一个返回值是—1、0 或者 1 的双参数函数。说它更高效,是因为在每个元素上,key函数只会被调用一次。而双参数比较函数则在每一次两两比较的时候都会被调用。诚然,在排序的时候,Python 总会比较两个键(key),但是那一阶段的计算会发生在 C 语言那一层,这样会比调用用户自定义的 Python 比较函数更快。另外,
key参数也能让你对一个混有数字字符和数值的列表进行排序。你只需要决定到底是把字符看作数值,还是把数值看作字符:>>> l = [28, 14, '28', 5, '9', '1', 0, 6, '23', 19] >>> sorted(l, key=int) [0, '1', 5, 6, '9', 14, 19, '23', 28, '28'] >>> sorted(l, key=str) [0, '1', 14, 19, '23', 28, '28', 5, 6, '9']Oracle、Google 和 Timbot 之间的八卦
sorted和list.sort背后的排序算法是 Timsort,它是一种自适应算法,会根据原始数据的顺序特点交替使用插入排序和归并排序,以达到最佳效率。这样的算法被证明是很有效的,因为来自真实世界的数据通常是有一定的顺序特点的。维基百科上有一个条目是关于这个算法的(https://en.wikipedia.org/wiki/Timsort)。Timsort 在 2002 年的时候首次用在 CPython 中;自 2009 年起,Java 和 Android 也开始使用这个算法。后面这个时间点如此广为人知,是因为在 Google 对 Sun 的侵权案中, Oracle 把 Timsort 中的一些相关代码当作了呈堂证供。详见“ Oracle v. Google—Day 14 Filings”一文(http://www.groklaw.net/articlebasic.php?story=20120510205659643)。
Timsort 的创始人是 Tim Peters,他同时也是一位高产的 Python 核心开发者。由于他贡献了太多代码,以至于很多人都说他其实是人工智能,他也就有了“Timbot”这一绰号。在“Python Humor”(https://www.python.org/doc/humor/#id9)里可以读到相关的故事。Tim 也是“Python 之禅”(
import this)的作者。