《Python高级编程(第2版)》之语法最佳实践


试读: www.epubit.com.cn/book/detail…
购书: item.jd.com/12241204.ht…

编写高效语法的能力会随着时间逐步提升。回头看看写的第一个程序,你可能就会赞成这个观点。正确的语法看起来赏心悦目,而错误的语法则使人烦恼。html


除了实现的算法与程序架构设计以外,还要特别注意的是,程序的写法也会严重影响它将来的发展。许多程序被丢弃并从头重写,就是由于难懂的语法、不清晰的API或不合常理的标准。python


不过Python在最近几年里发生了很大变化。所以,若是你被邻居(一个爱嫉妒的人,来自本地Ruby开发者用户组)绑架了一段时间,而且远离新闻,那么你可能会对Python的新特性感到吃惊。从最先版本到目前的3.5版,这门语言已经作了许多改进,变得更加清晰、更加整洁、也更容易编写。Python基础知识并无发生很大变化,但如今使用的工具更符合人们的使用习惯。程序员


本章将介绍如今这门语言的语法中最重要的元素,以及它们的使用技巧,以下所示。算法



  • 列表推导(list comprehension)。

  • 迭代器(iterator)和生成器(generator)。

  • 描述符(descriptor)和属性(property)。

  • 装饰器(decorator)。

  • withcontextlib


速度提高或内存使用的代码性能技巧将会在第十一、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以及可变的bytearraystr不一样,只能用字节做为序列值,即0 <= x < 256范围内的整数。一开始可能会有点糊涂,由于其打印结果与字符串很是类似:bash


>>> print(bytes([102, 111, 111]))
b'foo'复制代码


对于bytesbytearray,在转换为另外一种序列类型(例如listtuple)时能够显示出其原本面目:


>>> 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中没有任何语法上的意义。


前面的一些例子中已经提到过字节,但为了保持先后一致,咱们来明确介绍它的语法。字节也被单引号、双引号或三引号包围,但必须有一个bB前缀:


>>> 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)构造函数,建立一个新的字节序列。若是sourcestr类型,那么必须指定encoding参数,它没有默认值。encodingerrors参数的用法与str.encode()方法中的相同。


用相似方法能够将bytes表示的二进制数据转换成字符串:



  • 利用bytes.decode(encoding, errors)方法,用注册编解码器对字节进行解码。这一方法的参数含义及其默认值与str.encode()相同。

  • 利用str(source, encoding, error)构造函数,建立一个新的字符串实例。与bytes()构造函数相似,若是source是字节序列的话,必须指定str函数的encoding参数,它没有默认值。



技巧.tif 


命名——字节与字节字符串的对比 


因为Python 3中的变化,有些人倾向于将bytes实例称为字节字符串。这主要是因为历史缘由——Python 3中的bytes是与Python 2中的str类型最为接近的序列类型(但并不彻底相同)。不过bytes实例是字节序列,也不须要表示文本数据。因此为了不混淆,虽然bytes实例与字符串具备类似性,但建议始终将其称为bytes或字节序列。Python 3中字符串的概念是为文本数据准备的,如今始终是str类型。



1.实现细节


Python字符串是不可变的。字节序列也是如此。这一事实很重要,由于它既有优势又有缺点。它还会影响Python高效处理字符串的方式。因为不变性,字符串能够做为字典的键或set的元素,由于一旦初始化以后字符串的值就不会改变。另外一方面,每当须要修改过的字符串时(即便只是微小的修改),都须要建立一个全新的字符串实例。幸运的是,bytearraybytes的可变版本,不存在这样的问题。字节数组能够经过元素赋值来进行原处修改(无需建立新对象),其大小也能够像列表同样动态地变化(利用appendpopinseer等方法)。


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()方法或%运算符。若是代码段的性能不是很重要,或者优化字符串拼接节省的开销很小,那么推荐使用字符串格式化做为最佳方法。



技巧.tif 


常数折叠和窥孔优化程序 


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.deletedel删除一个元素——复杂度为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)


对于须要真正的链表(或者简单来讲,双端appendpop操做的复杂度都是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]复制代码


这种写法除了更加高效以外,也更加简短,涉及的语法元素也更少。在大型程序中,这意味着更少的错误,代码也更容易阅读和理解。



技巧.tif 


列表推导和内部数组调整大小 


有些Python程序员中会谣传这样的说法:每添加几个元素以后都要对表示列表对象的内部数组大小进行调整,这个问题能够用列表推导来解决。还有人说一次分配就能够将数组大小调整到刚恰好。不幸的是,这些说法都是不正确的。


解释器在对列表推导进行求值的过程当中并不知道最终结果容器的大小,也就没法为它预先分配数组的最终大小。所以,内部数组的从新分配方式与for循环中彻底相同。但在许多状况下,与普通循环相比,使用列表推导建立列表要更加整洁、更加快速。



(3)其余习语

Python习语的另外一个典型例子是使用enumerate(枚举)。在循环中使用序列时,这个内置函数能够很方便地获取其索引。如下面这段代码为例:


>>> i = 0
>>> for element in ['one', 'two', 'three']:
... print(i, element)
... i += 1
...
0 one
1 two
2 three复制代码


它能够替换为下面这段更短的代码:


>>> for i, element in enumerate(['one', 'two', 'three']):
... print(i, element)
...
0 one
1 two
2 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'
>>> third
100复制代码


解包还能够利用带星号的表达式获取单个变量中的多个元素,只要它的解释没有歧义便可。还能够对嵌套序列进行解包。特别是在遍历由序列构成的复杂数据结构时,这种方法很是实用。下面是一些更复杂的解包示例:


>>> # 带星号的表达式能够获取序列的剩余部分
>>> first, second, 复制代码rest = 0, 1, 2, 3
>>> first
0
>>> second
1
>>> rest
[2, 3]

>>> # 带星号的表达式能够获取序列的中间部分
>>> first, inner, last = 0, 1, 2, 3
>>> first
0
>>> inner
[1, 2]
>>> last
3

>>> # 嵌套解包
>>> (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'
>>> items
dictitems([('spam', 'eggs'), ('fizz', 'bazz'), ('foo', 'bar')])复制代码


视图对象既有旧的keys()values()items()方法返回的列表的特性,也有旧的iterkeys()itervalues()iteritems()方法返回的迭代器的特性。视图无需冗余地将全部值都保存在内存里(像列表那样),但你仍然能够获取其长度(使用len),也能够测试元素是否包含其中(使用in子句)。固然,视图是可迭代的。


最后一件重要的事情是,在keys()values()方法返回的视图中,键和值的顺序是彻底对应的。在Python 2中,若是你想保证获取的键和值顺序一致,那么在两次函数调用之间不能修改字典的内容。如今dict keysdict _ values是动态的,因此即便在调用keys()values()之间字典内容发生了变化,那么这两个视图的元素遍历顺序也是彻底一致的。


(1)实现细节

CPython使用伪随机探测(pseudo-random probing)的散列表(hash table)做为字典的底层数据结构。这彷佛是很是高深的实现细节,但在短时间内不太可能发生变化,因此程序员也能够把它当作一个有趣的事实来了解。


因为这一实现细节,只有可哈希的(hashable)对象才能做为字典的键。若是一个对象有一个在整个生命周期都不变的散列值(hash value),并且这个值能够与其余对象进行比较,那么这个对象就是可哈希的。Python全部不可变的内置类型都是可哈希的。可变类型(如列表、字典和集合)是不可哈希的,所以不能做为字典的键。定义可哈希类型的协议包括下面这两个方法。



  • hash :这一方法给出dict内部实现须要的散列值(整数)。对于用户自定义类的实例对象,这个值由id()给出。

  • eq :比较两个对象的值是否相等。对于用户自定义类,除了自身以外,全部实例对象默认不相等。


若是两个对象相等,那么它们的散列值必定相等。反之则不必定成立。这说明可能会发生散列冲突(hash collision),即散列值相等的两个对象可能并不相等。这是容许的,全部Python实现都必须解决散列冲突。CPython用开放定址法(open addressing)来解决这一冲突(en.wikipedia.org/wiki/Open_a…


字典的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()
odictkeys(['0', '1', '2', '3', '4'])复制代码


OrderedDict还有一些其余功能,例如利用popitem()方法在双端取出元素或者利用move to _ end()方法将指定元素移动到某一端。这种集合类型的完整参考可参见Python文档(docs.python.org/3/library/c…


还有很重要的一点是,在很是老的代码库中,可能会用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:字典子类,能够经过调用用户自定义的工厂函数来设置缺失值。



提示.tif  


第12章介绍了从collections模块选择集合类型的更多细节,也给出了关于什么时候使用这些集合类型的建议。



2.2 高级语法


在一种语言中,很难客观判断哪些语法元素属于高级语法。对于本章会讲到的高级语法元素,咱们会讲到这样的元素,它们不与任何特定的内置类型直接相关,并且在刚开始学习时相对难以掌握。对于Python中难以理解的特性,其中最多见的是:



  • 迭代器(iterator)。

  • 生成器(generator)。

  • 装饰器(decorator)。

  • 上下文管理器(context manager)。


2.2.1 迭代器


迭代器只不过是一个实现了迭代器协议的容器对象。它基于如下两个方法。



  • next :返回容器的下一个元素。

  • iter :返回迭代器自己。


迭代器能够利用内置的iter函数和一个序列来建立。看下面这个例子:


>>> i = iter('abc')
>>> next(i)
'a'
>>> next(i)
'b'
>>> next(i)
'c'
>>> next(i)
Traceback (most recent call last):
File "< input >", line 1, in < module >
StopIteration复制代码


当遍历完序列时,会引起一个StopIteration异常。这样迭代器就能够与循环兼容,由于能够捕获这个异常并中止循环。要建立自定义的迭代器,能够编写一个具备 next 方法的类,只要这个类提供返回迭代器实例的 iter 特殊方法:


class CountDown:
def init(self, step):
self.step = step
def next(self):
"""Return the next element."""
if self.step < = 0:
raise StopIteration
self.step -= 1
return self.step
def iter(self):
"""Return the iterator itself."""
return self复制代码

下面是这个迭代器的用法示例:


>>> for element in CountDown(4):
... print(element)
...
3
2
1
0复制代码


迭代器自己是一个底层的特性和概念,在程序中能够不用它。但它为生成器这一更有趣的特性提供了基础。


2.2.2 yield语句


生成器提供了一种优雅的方法,可让编写返回元素序列的函数所需的代码变得简单、高效。基于yield语句,生成器能够暂停函数并返回一个中间结果。该函数会保存执行上下文,稍后在必要时能够恢复。


举个例子,斐波纳契(Fibonacci)数列能够用生成器语法来实现。下列代码是来自于PEP 255(简单生成器)文档中的例子:


def fibonacci():
a, b = 0, 1
while True:
yield b
a, b = b, a + b复制代码

你能够用next()函数或for循环从生成器中获取新的元素,就像迭代器同样:


>>> fib = fibonacci()
>>> next(fib)
1
>>> next(fib)
1
>>> next(fib)
2
>>> [next(fib) for i in range(10)]
[3, 5, 8, 13, 21, 34, 55, 89, 144, 233]复制代码


这个函数返回一个generator对象,是特殊的迭代器,它知道如何保存执行上下文。它能够被无限次调用,每次都会生成序列的下一个元素。这种语法很简洁,算法可无限调用的性质并无影响代码的可读性。没必要提供使函数中止的方法。实际上,它看上去就像用伪代码设计的数列同样。


在社区中,生成器并不经常使用,由于开发人员还不习惯这种思考方式。多年来,开发人员已经习惯于使用直截了当的函数。每次你须要返回一个序列的函数或在循环中运行的函数时,都应该考虑使用生成器。当序列元素被传递到另外一个函数中以进行后续处理时,一次返回一个元素能够提升总体性能。


在这种状况下,用于处理一个元素的资源一般不如用于整个过程的资源重要。所以,它们能够保持位于底层,使程序更加高效。举个例子,斐波那契数列是无穷的,但用来生成它的生成器每次提供一个值,并不须要无限大的内存。一个常见的应用场景是使用生成器的数据流缓冲区。使用这些数据的第三方代码能够暂停、恢复和中止生成器,在开始这一过程以前无需导入全部数据。


举个例子,来自标准库的tokenize模块能够从文本流中生成令牌(token),并对处理过的每一行都返回一个迭代器,以供后续处理:


>>> import tokenize
>>> reader = open('hello.py').readline
>>> tokens = tokenize.generate_tokens(reader)
>>> next(tokens)
TokenInfo(type=57 (COMMENT), string='# -复制代码- coding: utf-8 --', start=(1,
0), end=(1, 23), line='# -
- coding: utf-8 --\n')
>>> next(tokens)
TokenInfo(type=58 (NL), string='\n', start=(1, 23), end=(1, 24), line='#
-
- coding: utf-8 --\n')
>>> next(tokens)
TokenInfo(type=1 (NAME), string='def', start=(2, 0), end=(2, 3),
line='def helloworld():\n')
复制代码


从这里能够看出,open遍历文件的每一行,而generate tokens则利用管道对其进行遍历,完成一些额外的工做。对于基于某些序列的数据转换算法而言,生成器还有助于下降算法复杂度并提升效率。把每一个序列看做一个iterator,而后再将其合并为一个高阶函数,这种方法能够有效避免函数变得庞大、丑陋、没有可读性。此外,这种方法还能够为整个处理链提供实时反馈。


在下面的示例中,每一个函数都定义了一个对序列的转换。而后将这些函数连接起来并应用。每次调用都将处理一个元素并返回其结果:


def power(values):
for value in values:
print('powering %s' % value)
yield value
def adder(values):
for value in values:
print('adding to %s' % value)
if value % 2 == 0:
yield value + 3
else:
yield value + 2复制代码

将这些生成器合并使用,可能的结果以下:


>>> elements = [1, 4, 7, 9, 12, 19]
>>> results = adder(power(elements))
>>> next(results)
powering 1
adding to 1
3
>>> next(results)
powering 4
adding to 4
7
>>> next(results)
powering 7
adding to 7
9复制代码



技巧.tif 


保持代码简单,而不是保持数据简单  


最好编写多个处理序列值的简单可迭代函数,而不要编写一个复杂函数,同时计算出整个集合的结果。



Python生成器的另外一个重要特性,就是可以利用next函数与调用的代码进行交互。yield变成了一个表达式,而值能够经过名为send的新方法来传递:


def psychologist():
print('Please tell me your problems')
while True:
answer = (yield)
if answer is not None:
if answer.endswith('?'):
print("Don't ask yourself too much questions")
elif 'good' in answer:
print("Ahh that's good, go on")
elif 'bad' in answer:
print("Don't be so negative")复制代码

下面是调用psychologist()函数的示例会话:


>>> free = psychologist()
>>> next(free)
Please tell me your problems
>>> free.send('I feel bad')
Don't be so negative
>>> free.send("Why I shouldn't ?")
Don't ask yourself too much questions
>>> free.send("ok then i should find what is good for me")
Ahh that's good, go on复制代码


send的做用和next相似,但会将函数定义内部传入的值变成yield的返回值。所以,这个函数能够根据客户端代码来改变自身行为。为完成这一行为,还添加了另外两个函数:throwclose。它们将向生成器抛出错误。



  • throw:容许客户端代码发送要抛出的任何类型的异常。

  • close:做用相同,但会引起特定的异常——GeneratorExit。在这种状况下,生成器函数必须再次引起GeneratorExitStopIteration



提示.tif  


生成器是Python中协程、异步并发等其余概念的基础,这些概念将在第13章介绍。



2.2.3 装饰器


Python装饰器的做用是使函数包装与方法包装(一个函数,接受函数并返回其加强函数)变得更容易阅读和理解。最初的使用场景是在方法定义的开头可以将其定义为类方法或静态方法。若是不用装饰器语法的话,定义可能会很是稀疏,而且不断重复:


class WithoutDecorators:
def some_static_method():
print("this is static method")
some_static_method = staticmethod(some_static_method)
def some_class_method(cls):
print("this is class method")
some_class_method = classmethod(some_class_method)复制代码

若是用装饰器语法重写的话,代码会更简短,也更容易理解:


class WithDecorators:
@staticmethod
def some_static_method():
print("this is static method")

@classmethod
def some_class_method(cls):
print("this is class method")复制代码

1.通常语法和可能的实现


装饰器一般是一个命名的对象(不容许使用lambda表达式),在被(装饰函数)调用时接受单一参数,并返回另外一个可调用对象。这里用的是“可调用(callable)”。而不是以前觉得的“函数”。装饰器一般在方法和函数的范围内进行讨论,但它的适用范围并不局限于此。事实上,任何可调用对象(任何实现了 call 方法的对象都是可调用的)均可以用做装饰器,它们返回的对象每每也不是简单的函数,而是实现了本身的 call 方法的更复杂的类的实例。


装饰器语法只是语法糖而已。看下面这种装饰器用法:


@some_decorator
def decorated_function():
pass复制代码

这种写法老是能够替换为显式的装饰器调用和函数的从新赋值:


def decorated_function():
pass
decorated_function = some_decorator(decorated_function)复制代码

可是,若是在一个函数上使用多个装饰器的话,后一种写法的可读性更差,也很是难以理解。



技巧.tif 


装饰器甚至不须要返回可调用对象! 


事实上,任何函数均可以用做装饰器,由于Python并无规定装饰器的返回类型。所以,将接受单一参数但不返回可调用对象的函数(例如str)用做装饰器,在语法上是彻底有效的。若是用户尝试调用这样装饰过的对象,最后终究会报错。无论怎样,针对这种装饰器语法能够作一些有趣的试验。



(1)做为一个函数

编写自定义装饰器有许多方法,但最简单的方法就是编写一个函数,返回包装原始函数调用的一个子函数。


通用模式以下:


def mydecorator(function):
def wrapped(复制代码args, kwargs):
# 在调用原始函数以前,作点什么
result = function(*args,
kwargs)
# 在函数调用以后,作点什么,
# 并返回结果
return result
# 返回wrapper做为装饰函数
return wrapped复制代码

(2)做为一个类

虽然装饰器几乎老是能够用函数实现,但在某些状况下,使用用户自定义类可能更好。若是装饰器须要复杂的参数化或者依赖于特定状态,那么这种说法每每是对的。


非参数化装饰器用做类的通用模式以下:


class DecoratorAsClass:
def init(self, function):
self.function = function

def call(self, args, **kwargs):
# 在调用原始函数以前,作点什么
result = self.function(
args, kwargs)
# 在调用函数以后,作点什么,
# 并返回结果
return result
复制代码

(3)参数化装饰器

在实际代码中一般须要使用参数化的装饰器。若是用函数做为装饰器的话,那么解决方法很简单:须要用到第二层包装。下面一个简单的装饰器示例,给定重复次数,每次被调用时都会重复执行一个装饰函数:


def repeat(number=3):
"""屡次重复执行装饰函数。

返回最后一次原始函数调用的值做为结果
:param number: 重复次数,默认值是3
"""
def actual_decorator(function):
def wrapper(*args, 复制代码kwargs):
result = None
for _ in range(number):
result = function(args, **kwargs)
return result
return wrapper
return actual_decorator
复制代码

这样定义的装饰器能够接受参数:


>>> @repeat(2)
... def foo():
... print("foo")
...
>>> foo()
foo
foo复制代码


注意,即便参数化装饰器的参数有默认值,但名字后面也必须加括号。带默认参数的装饰器的正确用法以下:


>>> @repeat()
... def bar():
... print("bar")
...
>>> bar()
bar
bar
bar复制代码


没加括号的话,在调用装饰函数时会出现如下错误:


>>> @repeat
... def bar():
... pass
...
>>> bar()
Traceback (most recent call last):
File "< input >", line 1, in < module >
TypeError: actual_decorator() missing 1 required positional
argument: 'function'复制代码


(4)保存内省的装饰器

使用装饰器的常见错误是在使用装饰器时不保存函数元数据(主要是文档字符串和原始函数名)。前面全部示例都存在这个问题。装饰器组合建立了一个新函数,并返回一个新对象,但却彻底没有考虑原始函数的标识。这将会使得调试这样装饰过的函数更加困难,也会破坏可能用到的大多数自动生成文档的工具,由于没法访问原始的文档字符串和函数签名。


但咱们来看一下细节。假设咱们有一个虚设的(dummy)装饰器,仅有装饰做用,还有其余一些被装饰的函数:


def dummy_decorator(function):
def wrapped(复制代码args, kwargs):
"""包装函数内部文档。"""
return function(*args,
kwargs)
return wrapped

@dummy_decorator
def function_with_importantdocstring():
"""这是咱们想要保存的重要文档字符串。"""
复制代码

若是咱们在Python交互式会话中查看function with important docstring(),会注意到它已经失去了原始名称和文档字符串:


>>> function_with_important_docstring.name
'wrapped'
>>> function_with_important_docstring.doc
'包装函数内部文档。'复制代码


解决这个问题的正确方法,就是使用functools模块内置的wraps()装饰器:


from functools import wraps

def preserving_decorator(function):
@wraps(function)
def wrapped(args, **kwargs):
"""包装函数内部文档。"""
return function(
args, kwargs)
return wrapped

@preserving_decorator
def function_with_important_docstring():
"""这是咱们想要保存的重要文档字符串。"""
复制代码

这样定义的装饰器能够保存重要的函数元数据:


>>> function_with_important_docstring.name
'function_with_important_docstring.'
>>> function_with_important_docstring.doc
'这是咱们想要保存的重要文档字符串。'复制代码


2.用法和有用的例子


因为装饰器在模块被首次读取时由解释器来加载,因此它们的使用应受限于通用的包装器(wrapper)。若是装饰器与方法的类或所加强的函数签名绑定,那么应该将其重构为常规的可调用对象,以免复杂性。在任何状况下,装饰器在处理API时,一个好的作法是将它们汇集在一个易于维护的模块中。


常见的装饰器模式以下所示。



  • 参数检查。

  • 缓存。

  • 代理。

  • 上下文提供者。


(1)参数检查

检查函数接受或返回的参数,在特定上下文中执行时可能有用。举个例子,若是一个函数要经过XML-RPC来调用,那么Python没法像静态语言那样直接提供其完整签名。当XML-RPC客户端请求函数签名时,就须要用这个功能来提供内省能力。



技巧.tif 


XML-RPC协议


XML-RPC协议是一种轻量级的远程过程调用(Remote Procedure Call)协议,经过HTTP使用XML对调用进行编码。对于简单的客户端-服务器交换,一般使用这种协议而不是SOAP。SOAP提供了列出全部可调用函数的页面(WSDL),XML-RPC与之不一样,并无可用函数的目录。该协议提出了一个扩展,能够用来发现服务器API,Python的xmlrpc模块实现了这一扩展(参见docs.python.org/3/library/x…



自定义装饰器能够提供这种类型的签名,并确保输入和输出表明自定义的签名参数:


rpcinfo = {}

def xmlrpc(in
=(), out=(type(None),)):
def _xmlrpc(function):
# 注册签名
func_name = function.name
rpc_info[funcname] = (in, out)
def _check_types(elements, types):
"""用来检查类型的子函数。"""
if len(elements) != len(types):
raise TypeError('argument count is wrong')
typed = enumerate(zip(elements, types))
for index, couple in typed:
arg, of_the_right_type = couple
if isinstance(arg, of_the_right_type):
continue
raise TypeError(
'arg #%d should be %s' % (index,
of_the_right_type))

# 包装过的函数
def xmlrpc(args): # 没有容许的关键词
# 检查输入的内容
checkable_args = args[1:] # 去掉self
_check_types(checkableargs, in)
# 运行函数
res = function(
args)
# 检查输出的内容
if not type(res) in (tuple, list):
checkable_res = (res,)
else:
checkable_res = res
_check_types(checkable_res, out)

# 函数及其类型检查成功
return res
return
xmlrpc
return xmlrpc复制代码

装饰器将函数注册到全局字典中,并将其参数和返回值保存在一个类型列表中。注意,这个示例作了很大的简化,为的是展现装饰器的参数检查功能。


使用示例以下:


class RPCView:
@xmlrpc((int, int)) # two int -> None
def meth1(self, int1, int2):
print('received %d and %d' % (int1, int2))

@xmlrpc((str,), (int,)) # string -> int
def meth2(self, phrase):
print('received %s' % phrase)
return 12复制代码

在实际读取时,这个类定义会填充rpc infos字典,并用于检查参数类型的特定环境中:


>>> rpc_info
{'meth2': ((< class 'str'>,), (< class 'int'>,)), 'meth1': ((< class
'int'>, < class 'int'>), (,))}
>>> my = RPCView()
>>> my.meth1(1, 2)
received 1 and 2
>>> my.meth2(2)
Traceback (most recent call last):
File "< input>", line 1, in < module>
File "< input>", line 26, in xmlrpc
File "< input>", line 20, in _check_types
TypeError: arg #0 should be < class 'str'>
复制代码


(2)缓存

缓存装饰器与参数检查十分类似,不过它重点是关注那些内部状态不会影响输出的函数。每组参数均可以连接到惟一的结果。这种编程风格是函数式编程(functional programming,参见en.wikipedia.org/wiki/Functi…


所以,缓存装饰器能够将输出与计算它所须要的参数放在一块儿,并在后续的调用中直接返回它。这种行为被称为memoizing(参见en.wikipedia.org/wiki/Memoiz…


import time
import hashlib
import pickle

cache = {}

def is_obsolete(entry, duration):
return time.time() - entry['time'] > duration

def compute_key(function, args, kw):
key = pickle.dumps((function.复制代码name, args, kw))
return hashlib.sha1(key).hexdigest()

def memoize(duration=10):
def _memoize(function):
def
memoize(*args, 复制代码kw):
key = compute_key(function, args, kw)

# 是否已经拥有它了?
if (key in cache and
not is_obsolete(cache[key], duration)):
print('we got a winner')
return cache[key]['value']
# 计算
result = function(args, **kw)
# 保存结果
cache[key] = {
'value': result,
'time': time.time()
}
return result
return memoize
return _memoize
复制代码

利用已排序的参数值来构建SHA哈希键,并将结果保存在一个全局字典中。利用pickle来创建hash,这是冻结全部做为参数传入的对象状态的快捷方式,以确保全部参数都知足要求。举个例子,若是用一个线程或套接字做为参数,那么会引起PicklingError(参见docs.python.org/3/library/p…duration参数的做用是,若是上一次函数调用已通过去了太长时间,那么它会使缓存值无效。


下面是一个使用示例:


>>> @memoize()
... def very_very_very_complex_stuff(a, b):
... # 若是在执行这个计算时计算机过热
... # 请考虑停止程序
... return a + b
...
>>> very_very_very_complex_stuff(2, 2)
4
>>> very_very_very_complex_stuff(2, 2)
we got a winner
4
>>> @memoize(1) # 1秒后令缓存失效
... def very_very_very_complex_stuff(a, b):
... return a + b
...
>>> very_very_very_complex_stuff(2, 2)
4
>>> very_very_very_complex_stuff(2, 2)
we got a winner
4
>>> cache
{'c2727f43c6e39b3694649ee0883234cf': {'value': 4, 'time':
1199734132.7102251)}
>>> time.sleep(2)
>>> very_very_very_complex_stuff(2, 2)
4复制代码


缓存代价高昂的函数能够显著提升程序的整体性能,但必须当心使用。缓存值还能够与函数自己绑定,以管理其做用域和生命周期,代替集中化的字典。但在任何状况下,更高效的装饰器会使用基于高级缓存算法的专用缓存库。



提示.tif  


第12章将会介绍与缓存相关的详细信息和技术。



(3)代理

代理装饰器使用全局机制来标记和注册函数。举个例子,一个根据当前用户来保护代码访问的安全层可使用集中式检查器和相关的可调用对象要求的权限来实现:


class User(object):
def 复制代码init(self, roles):
self.roles = roles

class Unauthorized(Exception):
pass

def protect(role):
def _protect(function):
def
protect(复制代码args, kw):
user = globals().get('user')
if user is None or role not in user.roles:
raise Unauthorized("I won't tell you")
return function(*args,
kw)
return protect
return _protect
复制代码

这一模型经常使用于Python Web框架中,用于定义可发布类的安全性。例如,Django提供装饰器来保护函数访问的安全。


下面是一个示例,当前用户被保存在一个全局变量中。在方法被访问时装饰器会检查他/她的角色:


>>> tarek = User(('admin', 'user'))
>>> bill = User(('user',))
>>> class MySecrets(object):
... @protect('admin')
... def waffle_recipe(self):
... print('use tons of butter!')
...
>>> these_are = MySecrets()
>>> user = tarek
>>> these_are.waffle_recipe()
use tons of butter!
>>> user = bill
>>> these_are.waffle_recipe()
Traceback (most recent call last):
File "< stdin>", line 1, in < module>
File "< stdin>", line 7, in wrap
main.Unauthorized: I won't tell you复制代码


(4)上下文提供者

上下文装饰器确保函数能够运行在正确的上下文中,或者在函数先后运行一些代码。换句话说,它设定并复位一个特定的执行环境。举个例子,当一个数据项须要在多个线程之间共享时,就要用一个锁来保护它避免屡次访问。这个锁能够在装饰器中编写,代码以下:


from threading import RLock
lock = RLock()

def synchronized(function):
def _synchronized(args, **kw):
lock.acquire()
try:
return function(
args, **kw)
finally:
lock.release()
return _synchronized

@synchronized
def thread_safe(): # 确保锁定资源
pass复制代码

上下文装饰器一般会被上下文管理器(with语句)替代,后者将在本章后面介绍。


2.2.4 上下文管理器——with语句


为了确保即便在出现错误的状况下也能运行某些清理代码,try...finally语句是颇有用的。这一语句有许多使用场景,例如:



  • 关闭一个文件。

  • 释放一个锁。

  • 建立一个临时的代码补丁。

  • 在特殊环境中运行受保护的代码。


with语句为这些使用场景下的代码块包装提供了一种简单方法。即便该代码块引起了异常,你也能够在其执行先后调用一些代码。例如,处理文件一般采用这种方式:


>>> hosts = open('/etc/hosts')
>>> try:
... for line in hosts:
... if line.startswith('#'):
... continue
... print(line.strip())
... finally:
... hosts.close()
...
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost复制代码



提示.tif  


本示例只针对Linux系统,由于要读取位于etc文件夹中的主机文件,但任何文本文件均可以用相同的方法来处理。



利用with语句,上述代码能够重写为:


>>> with open('/etc/hosts') as hosts:
... for line in hosts:
... if line.startswith('#'):
... continue
... print(line.strip())
...
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost复制代码


在前面的示例中,open的做用是上下文管理器,确保即便出现异常也要在执行完for循环以后关闭文件。


与这条语句兼容的其余项目是来自threading模块的类:



  • threading.Lock

  • threading.RLock

  • threading.Condition

  • threading.Semaphore

  • threading.BoundedSemaphore


通常语法和可能的实现


with语句的通常语法的最简单形式以下:


with context_manager:
# 代码块
...复制代码

此外,若是上下文管理器提供了上下文变量,能够用as子句保存为局部变量:


with context_manager as context:
# 代码块
...复制代码

注意,多个上下文管理器能够同时使用,以下所示:


with A() as a, B() as b:
...复制代码

这种写法等价于嵌套使用,以下所示:


with A() as a:
with B() as b:
...复制代码

(1)做为一个类

任何实现了上下文管理器协议(context manager protocol)的对象均可以用做上下文管理器。该协议包含两个特殊方法。



简而言之,执行with语句的过程以下:



  • 调用 enter 方法。任何返回值都会绑定到指定的as子句。

  • 执行内部代码块。

  • 调用 exit 方法。


exit 接受代码块中出现错误时填入的3个参数。若是没有出现错误,那么这3个参数都被设为None。出现错误时, exit 不该该从新引起这个错误,由于这是调用者(caller)的责任。但它能够经过返回True来避免引起异常。这可用于实现一些特殊的使用场景,例以下一节将会看到的contextmanager装饰器。但在大多数使用场景中,这一方法的正确行为是执行相似于finally子句的一些清理工做,不管代码块中发生了什么,它都不会返回任何内容。


下面是某个实现了这一协议的上下文管理器示例,以更好地说明其工做原理:


class ContextIllustration:
def 复制代码enter(self):
print('entering context')
def
exit(self, exc_type, exc_value, traceback):
print('leaving context')

if exc_type is None:
print('with no error')
else:
print('with an error (%s)' % exc_value)
复制代码

没有引起异常时的运行结果以下:


>>> with ContextIllustration():
... print("inside")
...
entering context
inside
leaving context
with no error复制代码


引起异常时的输出以下:


>>> with ContextIllustration():
... raise RuntimeError("raised within 'with'")
...
entering context
leaving context
with an error (raised within 'with')
Traceback (most recent call last):
File "< input >", line 2, in < module >
RuntimeError: raised within 'with'复制代码


(2)做为一个函数——contextlib模块

使用相似乎是实现Python语言提供的任何协议最灵活的方法,但对许多使用场景来讲可能样板太多。标准库中新增了contextlib模块,提供了与上下文管理器一块儿使用的辅助函数。它最有用的部分是contextmanager装饰器。你能够在一个函数里面同时提供 enter exit 两部分,中间用yield语句分开(注意,这样函数就变成了生成器)。用这个装饰器编写前面的例子,其代码以下:


from contextlib import contextmanager

@contextmanager
def contextillustration():
print('entering context')

try:
yield
except Exception as e:
print('leaving context')
print('with an error (%s)' % e)
# 须要再次抛出异常
raise
else:
print('leaving context')
print('with no error')
复制代码

若是出现任何异常,该函数都须要再次抛出这个异常,以便传递它。注意,context illustration在须要时能够有一些参数,只要在调用时提供这些参数便可。这个小的辅助函数简化了常规的基于类的上下文API,正如生成器对基于类的迭代器API的做用同样。


这个模块还提供了其余3个辅助函数。



  • closing(element):返回一个上下文管理器,在退出时会调用该元素的close方法。例如,它对处理流的类就颇有用。

  • supress(*exceptions):它会压制发生在with语句正文中的特定异常。

  • redirect stdout(new target)redirect stderr(new target):它会将代码块内任何代码的sys.stdoutsys.stderr输出重定向到类文件(file-like)对象的另外一个文件。


2.3 你可能还不知道的其余语法元素


Python语法中有一些元素不太常见,也不多用到。这是由于它们能提供的好处不多,或者它们的用法很难记住。所以,许多Python程序员(即便有多年的经验)彻底不知道这些语法元素的存在。其中最有名的例子以下:



  • for ... else语句。

  • 函数注解(function annotation)。


2.3.1 for ... else ...语句


for循环以后使用else子句,能够在循环“天然”结束而不是被break语句终止时执行一个代码块:


>>> for number in range(1):
... break
... else:
... print("no break")
...
>>>
>>> for number in range(1):
... pass
... else:
... print("break")
...
break复制代码


这一语句在某些状况下颇有用,由于它有助于删除一些“哨兵(sentinel)”变量,若是出现break时用户想要保存信息,可能会须要这些变量。这使得代码更加清晰,但可能会使不熟悉这种语法的程序员感到困惑。有人说else子句的这种含义是违反直觉的,但这里介绍一个简单的技巧,能够帮你记住它的用法:for循环以后else子句的含义是“没有break”。


2.3.2 函数注解


函数注解是Python 3最独特的功能之一。官方文档是这么说的:函数注解是关于用户自定义函数使用的类型的彻底可选的元信息,但事实上,它并不局限于类型提示,并且在Python及其标准库中也没有单个功能能够利用这种注解。这就是这个功能独特的缘由:它没有任何语法上的意义。能够为函数定义注解,并在运行时获取这些注解,但仅此而已。如何使用注解留给开发人员去思考。


1.通常语法


对Python官方文档中的示例稍做修改,就能够很好展现如何定义并获取函数注解:


>>> def f(ham: str, eggs: str = 'eggs') -> str:
... pass
...
>>> print(f.annotations)
{'return': < class 'str' >, 'eggs': < class 'str' >, 'ham': < class 'str' >}复制代码


如上所述,参数注解的定义为冒号后计算注解值的表达式。返回值注解的定义为表示def语句结尾的冒号与参数列表以后的-&gt;之间的表达式。


定义好以后,注解能够经过函数对象的 annotations __属性获取,它是一个字典,在应用运行期间能够获取。


任何表达式均可以用做注解,其位置靠近默认参数,这样能够建立一些迷惑人的函数定义,以下所示:


>>> def square(number: 0< =3 and 1=0) - > (\
... +9000): return number**2
>>> square(10)
100复制代码


不过,注解的这种用法只会让人糊涂,没有任何其余做用。即便不用注解,编写出难以阅读和理解的代码也是相对容易的。


2.可能的用法


虽然注解有很大的潜力,但并无被普遍使用。一篇介绍Python 3新增功能的文章(参见docs.python.org/3/whatsnew/…PEP 3107列出如下可能的使用场景:



  • 提供类型信息。

    • 类型检查。

    • 让IDE显示函数接受和返回的类型。

    • 函数重载/通用函数。

    • 与其余语言之间的桥梁。

    • 适配。

    • 谓词逻辑函数。

    • 数据库查询映射。

    • RPC参数编组。



  • 其余信息。

    • 参数和返回值的文档。




虽然函数注解存在的时间和Python 3同样长,但仍然很难找到任一常见且积极维护的包,将函数注解用做类型检查以外的功能。因此函数注解仍主要用于试验和玩耍,这也是Python 3最初发布时包含该功能的最初目的。


2.4 小结


本章介绍了不直接与Python类和面向对象编程相关的多个最佳语法实践。本章第一部分重点介绍了与Python序列和集合相关的语法特性,也讨论了字符串和字节相关的序列。本章其他部分介绍了两组独立的语法元素:一组是初学者相对难以理解的(例如迭代器、生成器和装饰器),另外一组是不为人知的(for...else子句和函数注解)。

</div>复制代码
相关文章
相关标签/搜索