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


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

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


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


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


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



  • 列表推导(list comprehension)。
  • 迭代器(iterator)和生成器(generator)。
  • 描述符(descriptor)和属性(property)。
  • 装饰器(decorator)。
  • withcontextlib

速度提升或内存使用的代码性能技巧将会在第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以及可变的bytearraystr不同,只能用字节作为序列值,即0 <= x < 256范围内的整数。一开始可能会有点糊涂,因为其打印结果与字符串非常相似:


>>> 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参数,它没有默认值。


 


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


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



 


常数折叠和窥孔优化程序 


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]复制代码


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



 


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


有些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:字典子类,可以通过调用用户自定义的工厂函数来设置缺失值。


 


第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复制代码



 


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


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



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


 


生成器是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)复制代码

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



 


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


事实上,任何函数都可以用作装饰器,因为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客户端请求函数签名时,就需要用这个功能来提供内省能力。



 


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复制代码


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



 


第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复制代码



 


本示例只针对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)的对象都可以用作上下文管理器。该协议包含两个特殊方法。



enter (self) :更多内容请访问docs.python.org/3.3/referen… #object.enter
exit (self, exc type, exc value, traceback) :更多内容请访问docs.python.org/3.3/referen….exit

简而言之,执行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>复制代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值