Python3.5的新书,这一本不可错过!
Python很棒!
从20世纪80年代末出现的最早版本到当前版本,Python的发展一直遵循着相同的理念:提供一个同时具备可读性和生产力的多范式编程语言。
人们曾经将Python看作另一种脚本语言,认为它不适合构建大型系统。但多年以来,在一些先驱公司的努力下,Python显然可以用于构建几乎任何类型的系统。
基于Python3.5新书,今天小编推荐《Python高级编程(第2版)》
编辑推荐
本书展现了作者多年构建各种Python应用的经验,从几个小时完成的小型系统脚本,到许多开发人员历经数年编写的大型应用。
本书描述了开发人员使用Python的实践。
本书包含了一些主题,这些主题并不关注语言本身,而是更多地关注如何利用相关的工具和技术。
换句话说,本书描述了高级Python开发人员每天的工作方式。
本书内容
第1章介绍了Python语言及其社区的现状。本章展示了Python不断变化的方式及原因,还解释了为什么这些事实对任何想要自称Python专家的人来说是很重要的。本章还介绍了最流行和最公认的Python工作方式——常用的生产力工具和现已成为标准的约定。
第2章深入介绍迭代器、生成器、描述符等内容。本章还包括关于Python习语和CPython类型内部实现的有用注释,这些类型的计算复杂度是对这些习语的阐释。
第3章介绍了语法最佳实践,但重点放在类级别以上。本章包括Python中更高级的面向对象的概念和机制。学习这些知识是为了理解本章最后一节的内容,其中介绍的是Python元编程的各种方法。
第4章介绍了如何选择好的名称。它是对PEP 8中命名最佳实践的扩展,并且给出了一些如何设计良好API的提示。
第5章介绍如何创建Python包以及使用哪些工具,以便在官方的Python包索引或其他包仓库中正确地分发。对于Python包还补充了一些工具的简要回顾,这些工具可以让你用Python源代码创建独立可执行文件。
第6章主要针对Python Web开发人员和后端工程师,因为讲的是代码部署。本章解释了如何构建Python应用,使其可以轻松部署到远程服务器,还介绍了可以将这个过程自动化的工具。本章是第5章的延续,因此还介绍了如何使用包和私有包仓库来简化应用部署。
第7章解释了为什么为Python编写C扩展程序有时可能是一个好的解决方案。本章还展示了只要使用了正确的工具,它并不像想象中那么难。
第8章深入介绍了项目代码库的管理方式,还介绍了如何设置各种持续开发流程。
第9章包含文档相关的内容,提供了有关技术写作和Python项目文档化方式的建议。
第10章解释了测试驱动开发的基本原理,还介绍了可用于这种开发方法的工具。
第11章解释了何为优化,介绍了分析技术和优化策略指南。
第12章是对第11章的扩展,为Python程序中经常出现的性能问题提供了一些常用的解决方案。
第13章介绍了Python并发这一宏大的主题。本章解释了并发的概念、何时需要编写并发应用,以及Python程序员主要使用的并发方法。
第14章用一套有用的设计模式以及Python的代码示例对本书进行了总结。
阅读本书的前提
本书面向的是可以在任何操作系统上使用Python 3进行软件开发的人员。
这不是一本面向初学者的书,所以我假设你已经在开发环境中安装了Python,或者知道如何安装Python。不管怎样,本书考虑到以下事实:不是每个人都需要充分了解Python的最新功能或官方推荐的工具。因此,第1章概述了常见的实用程序(例如虚拟环境和pip),这些实用程序现在已经成为Python专业开发人员的标准工具
谁适合读本书?
本书面向的是想要进一步掌握Python的开发人员。开发人员主要指的是专业人士,即用Python编写软件的程序员。这是因为本书主要侧重于工具和实践,它们对于创建高性能的、可靠且可维护的Python软件至关重要。
这并不意味着业余爱好者无法从本书中发现有趣的内容。对于任何对学习Python高级概念感兴趣的人来说,本书都是很棒的。任何具备Python基本技能的人都应该能够读懂本书的内容,虽然经验不足的程序员可能需要一些额外的努力。对于有点落后仍在继续使用Python 2.7或更老版本的人来说,本书也是对Python 3.5的全面介绍。
最后,从阅读本书中受益最多的人群应该是Web开发者和后端工程师。这是因为本书重点介绍了在他们的工作领域中特别重要的两个主题:可靠的代码部署与并发。
本书约定
本书用多种文本样式来区分不同种类的信息。下面是这些样式的示例及其含义解释。
文本中的代码、数据库表的名称、文件夹名称、文件名称、文件扩展名、路径名称、虚拟URL、用户输入和Twitter句柄的格式如下所示:“利用str.encode(encoding, errors)
方法,用注册编解码器对字符串进行编码。”
代码块的格式如下所示:
[print("hello world")
print "goodbye python2"
如果我们想让你将注意力集中在代码块的特定区域,相关的几行或几项将会被设成粗体,如下所示:
cdef long long fibonacci_cc(unsigned int n) nogil:
if n < 2:
return n
else:
return fibonacci_cc(n - 1) + fibonacci_cc(n - 2)
命令行的输入或输出如下所示:
$ pip show pip---Metadata-Version: 2.0Name: pipVersion: 7.1.2Summary: The PyPA recommended tool for installing Python packages.Home-page: https://pip.pypa.io/Author: The pip developersAuthor-email: python-virtualenv@groups.google.comLicense: MITLocation: /usr/lib/python2.7/site-packagesRequires:
新术语和重要词语将以粗体显示。你会在屏幕上看到的单词(例如在菜单或对话框中)将以下面这种文本形式出现:“单击Next按钮可跳转至下一屏”。
〓〗
警告或重要提示。
〓〗
提示和技巧。
读者反馈
我们十分欢迎读者的反馈意见。让我们了解你对本书的看法——喜欢哪些内容,不喜欢哪些内容。这些反馈对我们很重要,因为它有助于我们编写出对读者真正有帮助的书。
一般性的反馈请发送邮件至feedback@packtpub.com,并在邮件主题中注明本书的标题。
如果你是某个领域的专家,并且有兴趣写一本书或者参与出版一本书,请参阅我们的作者指南。
客户支持
现在你已经成为这本Packt图书的拥有者,为了让你的购买物超所值,我们还为你提供了许多其他方面的服务。
下载示例代码
你可以用自己的账号在Packt的官方网站下载本书的示例代码文件。如果你是在其他地方购买的本书,可以访问Packt的官方网站并注册,文件会直接通过邮件发送给你。
实际上,许多其他语言的开发者也醉心于Python,并将它作为首选语言。
试读第2章
编写高效语法的能力会随着时间逐步提高。回头看看写的第一个程序,你可能就会同意这个观点。正确的语法看起来赏心悦目,而错误的语法则令人烦恼。
除了实现的算法与程序架构设计之外,还要特别注意的是,程序的写法也会严重影响它未来的发展。许多程序被丢弃并从头重写,就是因为难懂的语法、不清晰的API或不合常理的标准。
不过Python在最近几年里发生了很大变化。因此,如果你被邻居(一个爱嫉妒的人,来自本地Ruby开发者用户组)绑架了一段时间,并且远离新闻,那么你可能会对Python的新特性感到吃惊。从最早版本到目前的3.5版,这门语言已经做了许多改进,变得更加清晰、更加整洁、也更容易编写。Python基础知识并没有发生很大变化,但现在使用的工具更符合人们的使用习惯。
本章将介绍现在这门语言的语法中最重要的元素,以及它们的使用技巧,如下所示。
列表推导(list comprehension)。
迭代器(iterator)和生成器(generator)。
描述符(descriptor)和属性(property)。
装饰器(decorator)。
with
和contextlib
。
速度提升或内存使用的代码性能技巧将会在第11、12章中讲述。
2.1 Python的内置类型
Python提供了许多好用的数据类型,既包括数字类型,也包括集合类型。对于数字类型来说,语法并没有什么特别之处。当然,每种类型的定义会有些许差异,也有一些(可能)不太有名的运算符细节,但留给开发人员的选择并不多。对于集合类型和字符串来说,情况就发生变化了。虽然人们常说“做事的方法应该只有一种”,但留给Python开发人员的选择确实有很多。在初学者看来,有些代码模式看起来既直观又简单,可是有经验的程序员往往会认为它们不够Pythonic,因为它们要么效率低下,要么就是过于啰嗦。
这种解决常见问题的Pythonic模式(许多程序员称之为习语[idiom])看起来往往只是美观而已。但这种看法大错特错。大多数习语都揭示了Python的内部实现方式以及内置结构和模块的工作原理。想要深入理解这门语言,了解更多这样的细节是很必要的。此外,社区本身也会受到关于Python工作原理的一些谣言和成见的影响。只有自己深入钻研,你才能够分辨出关于Python的流行说法的真假。
2.1.1 字符串与字节
对于只用Python 2编程的程序员来说,字符串的话题可能会造成一些困惑。Python 3中只有一种能够保存文本信息的数据类型,就是str
(string,字符串)。它是不可变的序列,保存的是Unicode码位(code point)。这是与Python 2的主要区别,Python 2用str
表示字节字符串,这种类型现在在Python 3中用bytes
对象来处理(但处理方式并不完全相同)。
Python中的字符串是序列。基于这一事实,应该把字符串放在其他容器类型的一节去介绍,但字符串与其他容器类型在细节上有一个很重要的差异。字符串可以保存的数据类型有非常明确的限制,就是Unicode文本。
bytes
以及可变的bytearray
与str
不同,只能用字节作为序列值,即0 <= x < 256
范围内的整数。一开始可能会有点糊涂,因为其打印结果与字符串非常相似:
>>> print(bytes([102, 111, 111]))b'foo'
对于bytes
和bytearray
,在转换为另一种序列类型(例如list
或tuple
)时可以显示出其本来面目:
>>> list(b'foo bar')[102, 111, 111, 32, 98, 97, 114]>>> tuple(b'foo bar')(102, 111, 111, 32, 98, 97, 114)
许多关于Python 3的争议都是关于打破字符串的向后兼容和Unicode的处理方式。从Python 3.0开始,所有没有前缀的字符串都是Unicode。因此,所有用单引号('
)、双引号("
)或成组的3个引号(单引号或双引号)包围且没有前缀的值都表示str
数据类型:
>>> type("some string")< class 'str' >
在Python 2中,Unicode需要有u
前缀(例如u"some string"
)。从Python 3.3开始,为保证向后兼容,仍然可以使用这个前缀,但它在Python 3中没有任何语法上的意义。
前面的一些例子中已经提到过字节,但为了保持前后一致,我们来明确介绍它的语法。字节也被单引号、双引号或三引号包围,但必须有一个b
或B
前缀:
>>> type(b"some bytes")< class 'bytes' >
注意,Python语法中没有bytearray
字面值。
最后同样重要的是,Unicode字符串中包含无法用字节表示的“抽象”文本。因此,如果Unicode字符串没有被编码为二进制数据的话,是无法保存在磁盘中或通过网络发送的。将字符串对象编码为字节序列的方法有两种:
利用
str.encode(encoding, errors)
方法,用注册编解码器(registered codec)对字符串进行编码。编解码器由encoding
参数指定,默认值为'utf-8'
。第二个errors
参数指定错误的处理方案,可以取'strict'
(默认值)、'ignore'
、'replace'
、'xmlcharrefreplace'
或其他任何注册的处理程序(参见内置codecs
模块的文档)。利用
bytes(source, encoding, errors)
构造函数,创建一个新的字节序列。如果source
是str
类型,那么必须指定encoding
参数,它没有默认值。encoding
和errors
参数的用法与str.encode()
方法中的相同。
用类似方法可以将bytes
表示的二进制数据转换成字符串:
利用
bytes.decode(encoding, errors)
方法,用注册编解码器对字节进行解码。这一方法的参数含义及其默认值与str.encode()
相同。利用
str(source, encoding, error)
构造函数,创建一个新的字符串实例。与bytes()
构造函数类似,如果source
是字节序列的话,必须指定str
函数的encoding
参数,它没有默认值。
命名——字节与字节字符串的对比
由于Python 3中的变化,有些人倾向于将
bytes
实例称为字节字符串。这主要是由于历史原因——Python 3中的bytes
是与Python 2中的str
类型最为接近的序列类型(但并不完全相同)。不过bytes
实例是字节序列,也不需要表示文本数据。所以为了避免混淆,虽然bytes
实例与字符串具有相似性,但建议始终将其称为bytes
或字节序列。Python 3中字符串的概念是为文本数据准备的,现在始终是str
类型。
1.实现细节
Python字符串是不可变的。字节序列也是如此。这一事实很重要,因为它既有优点又有缺点。它还会影响Python高效处理字符串的方式。由于不变性,字符串可以作为字典的键或set
的元素,因为一旦初始化之后字符串的值就不会改变。另一方面,每当需要修改过的字符串时(即使只是微小的修改),都需要创建一个全新的字符串实例。幸运的是,bytearray
是bytes
的可变版本,不存在这样的问题。字节数组可以通过元素赋值来进行原处修改(无需创建新对象),其大小也可以像列表一样动态地变化(利用append
、pop
、inseer
等方法)。
2.字符串拼接
由于Python字符串是不可变的,在需要合并多个字符串实例时可能会产生一些问题。如前所述,拼接任意不可变序列都会生成一个新的序列对象。思考下面这个例子,利用多个字符串的重复拼接操作来创建一个新字符串:
s = ""
for substring in substrings:
s += substring
这会导致运行时间成本与字符串总长度成二次函数关系。换句话说,这种方法效率极低。处理这种问题可以用str.join()
方法。它接受可迭代的字符串作为参数,返回合并后的字符串。由于这是一个方法,实际的做法是利用空字符串来调用它:
s = "".join(substrings)
字符串的这一方法还可以用于在需要合并的多个子字符串之间插入分隔符,看下面这个例子:
>>> ','.join(['some', 'comma', 'separated', 'values'])'some,comma,separated,values'
需要记住,仅仅因为join()
方法速度更快(对于大型列表来说更是如此),并不意味着在所有需要拼接两个字符串的情况下都应该使用这一方法。虽然这是一种广为认可的做法,但并不会提高代码的可读性。可读性是很重要的!在某些情况下,join()
的性能可能还不如利用加法的普通拼接,下面举几个例子。
如果子字符串的数量很少,而且已经包含在某个可迭代对象中,那么在某些情况下,创建一个新序列来进行拼接操作的开销可能会超过使用
join()
节省下来的开销。在拼接短的字面值时,由于CPython中的常数折叠(constant folding),一些复杂的字面值(不只是字符串)在编译时会被转换为更短的形式,例如
'a' + 'b' + 'c'
被转换为'abc'
。当然,这只适用于相对短的常量(字面值)。
最后,如果事先知道字符串的数目,可以用正确的字符串格式化方法来保证字符串拼接的最佳可读性。字符串格式化可以用str.format()
方法或%
运算符。如果代码段的性能不是很重要,或者优化字符串拼接节省的开销很小,那么推荐使用字符串格式化作为最佳方法。
常数折叠和窥孔优化程序
CPython对编译过的源代码使用窥孔优化程序来提高其性能。这种优化程序直接对Python字节码实现了许多常见的优化。如上所述,常数折叠就是其功能之一。生成常数的长度不得超过一个固定值。在Python 3.5中这个固定值仍然是 20。不管怎样,这个具体细节只是为了满足读者的好奇心而已,并不能在日常编程中使用。窥孔优化程序还实现了许多有趣的优化,详细信息请参见Python源代码中的
Python/peephole.c
文件。
2.1.2 集合类型
Python提供了许多内置的数据集合类型,如果选择明智的话,可以高效解决许多问题。你可能已经学过下面这些集合类型,它们都有专门的字面值,如下所示。
列表(list)。
元组(tuple)。
字典(dictionary)。
集合(set)
Python的集合类型当然不止这4种,它的标准库扩展了其可选列表。在许多情况下,问题的答案可能正如选择正确的数据结构一样简单。本书的这一部分将深入介绍各种集合类型,以帮你做出更好的选择。
1.列表与元组
Python最基本的两个集合类型就是列表与元组,它们都表示对象序列。只要是花几小时学过Python的人,应该都很容易发现二者之间的根本区别:列表是动态的,其大小可以改变;而元组是不可变的,一旦创建就不能修改。
虽然快速分配/释放小型对象的优化方法有很多,但对于元素位置本身也是信息的数据结构来说,推荐使用元组这一数据类型。举个例子,想要保存(x, y)坐标对,元组可能是一个很好的选择。反正关于元组的细节相当无趣。本章关于元组唯一重要的内容就是,tuple
是不可变的(immutable),因此也是可哈希的(hashable)。其具体含义将会在后面“字典”一节介绍。比元组更有趣的是另一种动态的数据结构list
,以及它的工作原理和高效处理理方式。
(1)实现细节
许多程序员容易将Python的list
类型与其他语言(如C、C++或Java)标准库中常见的链表的概念相混淆。事实上,CPython的列表根本不是列表。在CPython中,列表被实现为长度可变的数组。对于其他Python实现(如Jython和IronPython)而言,这种说法应该也是正确的,虽然这些项目的文档中没有记录其实现细节。造成这种混淆的原因很清楚。这种数据类型被命名为列表,还和链表实现有相似的接口。
为什么这一点很重要,这又意味着什么呢?列表是最常见的数据结构之一,其使用方式会对所有应用的性能带来极大影响。此外,CPython又是最常见也最常用的Python实现,所以了解其内部实现细节至关重要。
从细节上来看,Python中的列表是由对其他对象的引用组成的的连续数组。指向这个数组的指针及其长度被保存在一个列表头结构中。这意味着,每次添加或删除一个元素时,由引用组成的数组需要改变大小(重新分配)。幸运的是,Python在创建这些数组时采用了指数过分配(exponential over-allocation),所以并不是每次操作都需要改变数组大小。这也是添加或取出元素的平摊复杂度较低的原因。不幸的是,在普通链表中“代价很小”的其他一些操作在Python中的计算复杂度却相对较高:
利用
list.insert
方法在任意位置插入一个元素——复杂度为O(n)。利用
list.delete
或del
删除一个元素——复杂度为O(n)。
这里n是列表的长度。至少利用索引来查找或修改元素的时间开销与列表大小无关。表2-1是一张完整的表格,列出了大多数列表操作的平均时间复杂度。
表2-1
操作 | 复杂度 |
---|---|
复制 | O(n) |
添加元素 | O(1) |
插入元素 | O(n) |
获取元素 | O(1) |
修改元素 | O(1) |
删除元素 | O(n) |
遍历 | O(n) |
获取长度为k的切片 | O(k) |
删除切片 | O(n) |
修改长度为k的切片 | O(k+n) |
列表扩展(Extend) | O(k) |
乘以k | O(nk) |
测试元素是否在列表中(element in list) | O(n) |
min()/max() | O(n) |
获取列表长度 | O(1) |
对于需要真正的链表(或者简单来说,双端append
和pop
操作的复杂度都是O(1)的数据结构)的场景,Python在内置的collections
模块中提供了deque
(双端队列)。它是栈和队列的一般化,在需要用到双向链表的地方都可以使用这种数据结构。
(2)列表推导
你可能知道,编写这样的代码是很痛苦的:
>>> evens = []>>> for i in range(10):... if i % 2 == 0:... evens.append(i)...>>> evens[0, 2, 4, 6, 8]
这种写法可能适用于C语言,但在Python中的实际运行速度很慢,原因如下。
解释器在每次循环中都需要判断序列中的哪一部分需要修改。
需要用一个计数器来跟踪需要处理的元素。
由于
append()
是一个列表方法,所以每次遍历时还需要额外执行一个查询函数。
列表推导正是解决这个问题的正确方法。它使用编排好的功能对上述语法的一部分做了自动化处理:
>>> [i for i in range(10) if i % 2 == 0][0, 2, 4, 6, 8]
这种写法除了更加高效之外,也更加简短,涉及的语法元素也更少。在大型程序中,这意味着更少的错误,代码也更容易阅读和理解。
列表推导和内部数组调整大小
有些Python程序员中会谣传这样的说法:每添加几个元素之后都要对表示列表对象的内部数组大小进行调整,这个问题可以用列表推导来解决。还有人说一次分配就可以将数组大小调整到刚刚好。不幸的是,这些说法都是不正确的。
解释器在对列表推导进行求值的过程中并不知道最终结果容器的大小,也就无法为它预先分配数组的最终大小。因此,内部数组的重新分配方式与
for
循环中完全相同。但在许多情况下,与普通循环相比,使用列表推导创建列表要更加整洁、更加快速。
(3)其他习语
Python习语的另一个典型例子是使用enumerate
(枚举)。在循环中使用序列时,这个内置函数可以很方便地获取其索引。以下面这段代码为例:
>>> i = 0>>> for element in ['one', 'two', 'three']:... print(i, element)... i += 1...0 one1 two2 three
它可以替换为下面这段更短的代码:
>>> for i, element in enumerate(['one', 'two', 'three']):... print(i, element)...0 one1 two2 three
如果需要一个一个合并多个列表(或任意可迭代对象)中的元素,那么可以使用内置的zip()
函数。对两个大小相等的可迭代对象进行均匀遍历时,这是一种非常常用的模式:
>>> for item in zip([1, 2, 3], [4, 5, 6]):... print(item)...(1, 4)(2, 5) (3, 6)
注意,对zip()
函数返回的结果再次调用zip()
,可以将其恢复原状:
>>> for item in zip(*zip([1, 2, 3], [4, 5, 6])):... print(item)...(1, 2, 3)(4, 5, 6)
另一个常用的语法元素是序列解包(sequence unpacking)。这种方法并不限于列表和元组,而是适用于任意序列类型(甚至包括字符串和字节序列)。只要赋值运算符左边的变量数目与序列中的元素数目相等,你都可以用这种方法将元素序列解包到另一组变量中:
>>> first, second, third = "foo", "bar", 100>>> first'foo'>>> second'bar'>>> third100
解包还可以利用带星号的表达式获取单个变量中的多个元素,只要它的解释没有歧义即可。还可以对嵌套序列进行解包。特别是在遍历由序列构成的复杂数据结构时,这种方法非常实用。下面是一些更复杂的解包示例:
>>> # 带星号的表达式可以获取序列的剩余部分>>> first, second, *rest = 0, 1, 2, 3>>> first0>>> second1>>> rest[2, 3]>>> # 带星号的表达式可以获取序列的中间部分>>> first, *inner, last = 0, 1, 2, 3>>> first0>>> inner[1, 2]>>> last3>>> # 嵌套解包>>> (a, b), (c, d) = (1, 2), (3, 4)>>> a, b, c, d(1, 2, 3, 4)
2.字典
字典是Python中最通用的数据结构之一。dict
可以将一组唯一键映射到对应的值,如下所示:
{
1: ' one',
2: ' two',
3: ' three',
}
字典是你应该已经了解的基本内容。不管怎样,程序员还可以用和前面列表推导类似的推导来创建一个新的字典。这里有一个非常简单的例子如下所示:
squares = {number: number**2 for number in range(100)}
重要的是,使用字典推导具有与列表推导相同的优点。因此在许多情况下,字典推导要更加高效、更加简短、更加整洁。对于更复杂的代码而言,需要用到许多if
语句或函数调用来创建一个字典,这时最好使用简单的for
循环,尤其是它还提高了可读性。
对于刚刚接触Python 3的Python程序员来说,在遍历字典元素时有一点需要特别注意。字典的keys()
、values()
和items()
3个方法的返回值类型不再是列表。此外,与之对应的iterkeys()
、itervalues()
和iteritems()
本来返回的是迭代器,而Python 3中并没有这3个方法。现在keys()
、values()
和items()
返回的是视图对象(view objects)。
keys()
:返回dict _ keys
对象,可以查看字典的所有键。values()
:返回dict _ values
对象,可以查看字典的所有值。it ems()
:返回dict _ items
对象,可以查看字典所有的(key, value)
二元元组。
视图对象可以动态查看字典的内容,因此每次字典发生变化时,视图都会相应改变,见下面这个例子:
>>> words = {'foo': 'bar', 'fizz': 'bazz'}>>> items = words.items()>>> words['spam'] = 'eggs'>>> itemsdict_items([('spam', 'eggs'), ('fizz', 'bazz'), ('foo', 'bar')])
视图对象既有旧的keys()
、values()
和items()
方法返回的列表的特性,也有旧的iterkeys()
、itervalues()
和iteritems()
方法返回的迭代器的特性。视图无需冗余地将所有值都保存在内存里(像列表那样),但你仍然可以获取其长度(使用len
),也可以测试元素是否包含其中(使用in
子句)。当然,视图是可迭代的。
最后一件重要的事情是,在keys()
和values()
方法返回的视图中,键和值的顺序是完全对应的。在Python 2中,如果你想保证获取的键和值顺序一致,那么在两次函数调用之间不能修改字典的内容。现在dict _ keys
和dict _ values
是动态的,所以即使在调用keys()
和values()
之间字典内容发生了变化,那么这两个视图的元素遍历顺序也是完全一致的。
(1)实现细节
CPython使用伪随机探测(pseudo-random probing)的散列表(hash table)作为字典的底层数据结构。这似乎是非常高深的实现细节,但在短期内不太可能发生变化,所以程序员也可以把它当做一个有趣的事实来了解。
由于这一实现细节,只有可哈希的(hashable)对象才能作为字典的键。如果一个对象有一个在整个生命周期都不变的散列值(hash value),而且这个值可以与其他对象进行比较,那么这个对象就是可哈希的。Python所有不可变的内置类型都是可哈希的。可变类型(如列表、字典和集合)是不可哈希的,因此不能作为字典的键。定义可哈希类型的协议包括下面这两个方法。
__ hash __
:这一方法给出dict
内部实现需要的散列值(整数)。对于用户自定义类的实例对象,这个值由id()
给出。__ eq __
:比较两个对象的值是否相等。对于用户自定义类,除了自身之外,所有实例对象默认不相等。
如果两个对象相等,那么它们的散列值一定相等。反之则不一定成立。这说明可能会发生散列冲突(hash collision),即散列值相等的两个对象可能并不相等。这是允许的,所有Python实现都必须解决散列冲突。CPython用开放定址法(open addressing)来解决这一冲突(https://en.wikipedia.org/wiki/Open_addressing)。不过,发生冲突的概率对性能有很大影响,如果概率很高,字典将无法从其内部优化中受益。
字典的3个基本操作(添加元素、获取元素和删除元素)的平均时间复杂度为O(1),但它们的平摊最坏情况复杂度要高得多,为O(n),这里的n是当前字典的元素数目。此外,如果字典的键是用户自定义类的对象,并且散列方法不正确的话(发生冲突的风险很大),那么这会给字典性能带来巨大的负面影响。CPython字典的时间复杂度的完整表格如表2-2所示。
表2-2
操作 | 平均复杂度 | 平摊最坏情况复杂度 |
---|---|---|
获取元素 | O(1) | O(n) |
修改元素 | O(1) | O(n) |
删除元素 | O(1) | O(n) |
复制 | O(n) | O(n) |
遍历 | O(n) | O(n) |
还有很重要的一点需要注意,在复制和遍历字典的操作中,最坏情况复杂度中的n是字典曾经达到的最大元素数目,而不是当前元素数目。换句话说,如果一个字典曾经元素个数很多,后来又大大减少了,那么遍历这个字典可能要花费相当长的时间。因此在某些情况下,如果需要频繁遍历某个字典,那么最好创建一个新的字典对象,而不是仅在旧字典中删除元素。
(2)缺点和替代方案
使用字典的常见陷阱之一,就是它并不会按照键的添加顺序来保存元素的顺序。在某些情况下,字典的键是连续的,对应的散列值也是连续值(例如整数),那么由于字典的内部实现,元素的顺序可能和添加顺序相同:
>>> {number: None for number in range(5)}.keys()dict_keys([0, 1, 2, 3, 4])
不过,如果使用散列方法不同的其他数据类型,那么字典就不会保存元素顺序。下面是CPython中的例子:
>>> {str(number): None for number in range(5)}.keys()dict_keys(['1', '2', '4', '0', '3'])>>> {str(number): None for number in reversed(range(5))}.keys()dict_keys(['2', '3', '1', '4', '0'])
如上述代码所示,字典元素的顺序既与对象的散列方法无关,也与元素的添加顺序无关。但我们也不能完全信赖这一说法,因为在不同的Python实现中可能会有所不同。
但在某些情况下,开发者可能需要使用能够保存添加顺序的字典。幸运的是,Python标准库的collections
模块提供了名为OrderedDict
的有序字典。它选择性地接受一个可迭代对象作为初始化参数:
>>> from collections import OrderedDict>>> OrderedDict((str(number), None) for number in range(5)).keys()odict_keys(['0', '1', '2', '3', '4'])
OrderedDict
还有一些其他功能,例如利用popitem()
方法在双端取出元素或者利用move _ to _ end()
方法将指定元素移动到某一端。这种集合类型的完整参考可参见Python文档(https://docs.python.org/3/library/collections.html)。
还有很重要的一点是,在非常老的代码库中,可能会用dict
来实现原始的集合,以确保元素的唯一性。虽然这种方法可以给出正确的结果,但只有在低于2.3的Python版本中才予以考虑。字典的这种用法十分浪费资源。Python有内置的set
类型专门用于这个目的。事实上,CPython中set
的内部实现与字典非常类似,但还提供了一些其他功能,以及与集合相关的特定优化。
3.集合
集合是一种鲁棒性很好的数据结构,当元素顺序的重要性不如元素的唯一性和测试元素是否包含在集合中的效率时,大部分情况下这种数据结构是很有用的。它与数学上的集合概念非常类似。Python的内置集合类型有两种。
set()
:一种可变的、无序的、有限的集合,其元素是唯一的、不可变的(可哈希的)对象。frozenset()
:一种不可变的、可哈希的、无序的集合,其元素是唯一的、不可变的(可哈希的)对象。
由于frozenset()
具有不变性,它可以用作字典的键,也可以作为其他set()
和frozenset()
的元素。在一个set()
或frozenset()
中不能包含另一个普通的可变set()
,因为这会引发TypeError
:
>>> set([set([1,2,3]), set([2,3,4])])Traceback (most recent call last): File "< stdin >", line 1, in < module >TypeError: unhashable type: 'set'
下面这种集合初始化的方法是完全正确的:
>>> set([frozenset([1,2,3]), frozenset([2,3,4])]){frozenset({1, 2, 3}), frozenset({2, 3, 4})}>>> frozenset([frozenset([1,2,3]), frozenset([2,3,4])])frozenset({frozenset({1, 2, 3}), frozenset({2, 3, 4})})
创建可变集合方法有以下3种,如下所示。
调用
set()
,选择性地接受可迭代对象作为初始化参数,例如set([0, 1, 2])
。使用集合推导,例如
{element for element in range(3)}
。使用集合字面值,例如
{1, 2, 3}
。
注意,使用集合的字面值和推导要格外小心,因为它们在形式上与字典的字面值和推导非常相似。此外,空的集合对象是没有字面值的。空的花括号{}
表示的是空的字典字面值。
实现细节
CPython中的集合与字典非常相似。事实上,集合被实现为带有空值的字典,只有键才是实际的集合元素。此外,集合还利用这种没有值的映射做了其他优化。
由于这一点,可以快速向集合添加元素、删除元素或检查元素是否存在,平均时间复杂度均为O(1)。但由于CPython的集合实现依赖于类似的散列表结构,因此这些操作的最坏情况复杂度是O(n),其中n是集合的当前大小。
字典的其他实现细节也适用于集合。集合中的元素必须是可哈希的,如果集合中用户自定义类的实例的散列方法不佳,那么将会对性能产生负面影响。
4.超越基础集合类型——collections
模块
每种数据结构都有其缺点。没有一种集合类型适合解决所有问题,4种基本类型(元组、列表、集合和字典)提供的选择也不算多。它们是最基本也是最重要的集合类型,都有专门的语法。幸运的是,Python标准库内置的collections
模块提供了更多的选择。前面已经提到过其中一种(deque
)。下面是这个模块中最重要的集合类型。
namedtuple()
:用于创建元组子类的工厂函数(factory function),可以通过属性名来访问它的元索引。deque
:双端队列,类似列表,是栈和队列的一般化,可以在两端快速添加或取出元素。ChainMap
:类似字典的类,用于创建多个映射的单一视图。Counter
:字典子类,由于对可哈希对象进行计数。OrderedDict
:字典子类,可以保存元素的添加顺序。defaultdict
:字典子类,可以通过调用用户自定义的工厂函数来设置缺失值。
第12章介绍了从
collections
模块选择集合类型的更多细节,也给出了关于何时使用这些集合类型的建议。长期福利
规则:1.将【异步图书】公众号推荐给你的朋友
2.关注【异步图书】大于等于10个人且关注时间超过10天
3.将朋友微信昵称或者截图发送至异步图书后台
4.经小编确认后,会赠出任意100元以下“异步图书”一本
5.本活动长期有效,每个读者限领取一次。
6.参与活动的读者需要在好友关注 “异步图书”10天后将昵称发给小编确认
点击下方阅读原文,即可购买《Python高级编程(第2版)》