Skip to content

Latest commit

 

History

History
1410 lines (1019 loc) · 61.7 KB

File metadata and controls

1410 lines (1019 loc) · 61.7 KB

二、语法最佳实践——低于类级别

随着时间的推移,编写高效语法的能力自然而然地就会出现。如果你回顾一下你的第一个项目,你可能会同意这一点。在你看来,正确的语法是一段好看的代码,而错误的语法是令人不安的。

除了所实现的算法和程序的体系结构设计之外,对其编写方式的高度关注也会对其发展产生重大影响。许多程序因为语法迟钝、API 不清晰或非传统标准而被抛弃并从头重写。

但是 Python 在过去几年中已经有了很大的发展。因此,如果你被邻居绑架了一段时间(当地 Ruby 开发者用户组的一个嫉妒的家伙),并且远离新闻,你可能会对它的新特性感到惊讶。从最早的版本到当前的版本(现在是 3.5),已经做了很多改进,以使语言更清晰、更清晰、更易于编写。Python 的基础并没有发生巨大的变化,但是使用它们的工具现在更加符合人体工程学。

本章介绍现代语法的最重要元素及其用法提示:

  • 列表解析
  • 迭代器和生成器
  • 描述符和属性
  • 装饰师
  • withcontextlib

第 11 章优化——一般原则和分析技术第 12 章优化——一些强大的技术中介绍了提高速度或内存使用的代码性能技巧。

Python 的内置类型

Python 提供了一组非常好的数据类型。这对数值类型和集合都适用。关于数字类型,它们的语法没有什么特别之处。当然,在定义每种类型的文本和一些(可能)不为人所知的关于运算符的细节方面存在一些差异,但留给开发人员的选择并不多。当涉及到集合和字符串时,情况会发生变化。尽管有“应该只有一种方法做某事”的口号,Python 开发人员仍然有很多选择。有些代码模式对于初学者来说是直观和简单的,但经验丰富的程序员通常认为它们不是Pythonic,因为它们要么效率低下,要么过于冗长。

这种用于解决常见问题的Pythonic模式(被许多程序员称为惯用语)通常看起来只是一种美学。这是错误的。大多数习惯用法都是由 Python 是如何在内部实现的以及内置结构和模块是如何工作的这一事实驱动的。了解更多这样的细节对于更好地理解语言至关重要。此外,社区本身也不能摆脱关于 Python 工作方式的神话和定型观念。只有自己深入挖掘,才能知道哪些流行的关于 Python 的说法是真的。

字符串和字节

字符串的主题可能会让那些习惯于只在 Python 2 中编程的程序员感到困惑。在 Python3 中,只有一种数据类型能够存储文本信息。它是str或者简单地说是字符串。它是一个存储 Unicode 代码点的不可变序列。这是与 Python2 的主要区别,Python2 中的str表示字节字符串—现在由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)

Python3 的许多争议是关于破坏字符串文本的向后兼容性以及如何处理 Unicode。从 Python 3.0 开始,每个不带前缀的字符串文字都是 Unicode。因此,由单引号('、双引号(")或由三个引号组成的组(单引号或双引号)括起来的文本(不带任何前缀)表示str数据类型:

>>> type("some string")
<class 'str'>

在 Python 2 中,Unicode 文本需要u前缀(如u"some string")。这个前缀仍然允许向后兼容(从 Python3.3 开始),但在 Python3 中没有任何语法含义。

在前面的一些示例中已经显示了字节文本,但是为了保持一致性,让我们显式地显示它的语法。字节文字也用单引号、双引号、双引号或三引号括起来,但前面必须有一个前缀bB

>>> type(b"some bytes")
<class 'bytes'>

请注意,Python 语法中没有bytearray文本。

最后,Unicode 字符串包含独立于字节表示的“抽象”文本。这使得它们无法保存在磁盘上或通过网络发送而不编码为二进制数据。有两种方法可以将字符串对象编码为字节序列:

  • 使用str.encode(encoding, errors)方法,使用注册的编码解码器对字符串进行编码。编解码器使用encoding参数指定,默认情况下为'utf-8'。第二个 errors 参数指定错误处理方案。它可以是'strict'(默认)、'ignore''replace''xmlcharrefreplace'或任何其他已注册的处理程序(请参阅内置的codecs模块文档)。
  • 使用bytes(source, encoding, errors)构造函数,创建一个新的字节序列。当源为str类型时,encoding参数是必须的,并且没有默认值。encodingerrors参数的用法与str.encode()方法相同。

bytes表示的二进制数据可以通过类似的方式转换为字符串:

  • 使用bytes.decode(encoding, errors)方法,使用注册编码的编解码器对字节进行解码。此方法的参数与str.encode()的参数具有相同的含义和默认值。
  • 使用str(source, encoding, error)构造函数,创建一个新的字符串实例。与bytes()构造函数类似,str()调用中的encoding参数没有默认值,如果字节序列用作源,则必须提供该参数。

提示

命名–字节与字节字符串

由于 Python3 中的更改,一些人倾向于将bytes实例称为字节字符串。这主要是由于历史原因——Python3 中的bytes是与 Python2 中的str类型最接近的序列类型(但不同)。不过,bytes实例是一个字节序列,也不需要表示文本数据。因此,为了避免任何混淆,建议始终将它们称为bytes或字节序列,尽管它们与字符串相似。在 Python3 中,字符串的概念是为文本数据保留的,现在总是str

实施细则

Python 字符串是不可变的。字节序列也是如此。这是一个重要的事实,因为它既有优点也有缺点。它还影响 Python 中有效处理字符串的方式。由于不变性,字符串可以用作字典键或set集合元素,因为一旦初始化,它们将永远不会更改其值。另一方面,每当需要修改字符串时(即使只做了很小的修改),都需要创建一个全新的实例。幸运的是,bytearray作为bytes的可变版本并没有引入这样的问题。字节数组可以通过项分配进行就地修改(无需创建新对象),并且可以像使用附加、弹出、插入等的列表一样动态调整大小。

字符串串联

当需要将多个字符串实例连接在一起时,知道 Python 字符串是不可变的这一事实会带来一些问题。如前所述,连接任何不可变序列会导致创建新的序列对象。考虑到一个新字符串是由多个字符串的重复级联建立的,如下:

s = ""
for substring in substrings:
    s += substring

这将导致字符串总长度的二次运行时成本。换句话说,这是非常低效的。对于此类情况的处理,有str.join()方法可用。它接受字符串的 iterable 作为参数,并返回一个连接的字符串。因为它是方法,所以实际的习惯用法使用空字符串文字作为方法的来源:

s = "".join(substrings)

提供此方法的字符串将用作连接的子字符串之间的分隔符;考虑下面的例子:

>>> ','.join(['some', 'comma', 'separated', 'values'])
'some,comma,separated,values'

值得记住的是,仅仅因为它更快(特别是对于大型列表),并不意味着join()方法应该在需要连接两个字符串的所有情况下使用。尽管它是一种被广泛认可的习惯用法,但它并不能提高代码的可读性——而且可读性很重要!在某些情况下,join()的性能可能不如通过加法实现的普通级联。这里有一些例子:

  • 如果子字符串的数量很小,并且在某些情况下它们还没有包含在某个 iterable 中,那么创建新序列只是为了执行串联的开销可能会掩盖使用join()的好处。
  • 当连接短文本时,由于 CPython 中的不断折叠,一些复杂的文本(不仅仅是字符串)如'a' + 'b' + 'c''abc'可以在编译时转换为较短的形式。当然,这仅对相对较短的常量(文本)启用。

最终,如果事先知道字符串的数量,则通过使用str.format()方法或%操作符,通过正确的字符串格式确保字符串连接的最佳可读性。在性能不重要或优化字符串连接的收益很少的代码段中,建议使用字符串格式作为最佳选择。

提示

恒定折叠和窥视孔优化器

CPython 在编译的源代码上使用窥视孔优化器以提高性能。这个优化器直接在 Python 的字节码上实现了许多常见的优化。如前所述,不断折叠就是这样一个特征。产生的常数在长度上受到硬编码值的限制。在 Python3.5 中,它始终等于 20。无论如何,这个特定的细节是一种好奇心,而不是日常编程中可以依赖的东西。窥视孔优化器执行的其他有趣优化的信息可以在 Python 源代码的Python/peephole.c文件中找到。

收藏

Python 提供了一个很好的内置数据收集选项,如果您选择得当,它可以让您高效地解决许多问题。您可能已经知道的类型是那些具有专用文本的类型:

  • 列表
  • 多元组
  • 辞典
  • 设置

Python 当然不限于这四种语言,它通过其标准库扩展了可能的选择列表。在许多情况下,问题的解决方案可能非常简单,只需对数据结构做出正确的选择。本书的这一部分旨在通过对可能的选择提供更深入的见解来简化这一决定。

列表和元组

Python 中最基本的两种集合类型是列表和元组,它们都表示对象序列。对于那些花了几个小时以上时间使用 Python 列表的人来说,它们之间的基本区别应该是显而易见的,因为 Python 列表是动态的,所以可以改变它们的大小,而元组是不可变的(它们在创建之后不能被修改】。

元组尽管有许多使小对象的分配/解除分配更快的优化,但对于元素位置本身就是信息的结构,元组是推荐的数据类型。例如,元组可能是存储一对(x,y)坐标的好选择。无论如何,关于元组的细节是相当乏味的。在本章的范围内,关于它们唯一重要的一点是tuple不可变的,因此是可散列的。这的含义将在词典一节中介绍。比元组更有趣的是它的动态对应物list,它到底是如何工作的,以及如何有效地处理它。

实施细则

很多程序员都很容易混淆 Python 的 To.T0.Type 类型,它经常出现在其他语言的标准库中,例如 C、C++或 java。事实上,CPython 列表根本不是列表。在 CPython 中,列表被实现为可变长度数组。对于 Jython 和 IronPython 等其他实现也应该如此,尽管这些项目中通常没有记录此类实现细节。造成这种混乱的原因很清楚。这个数据类型被命名为列表,并且还有一个可以从任何链表实现中预期的接口。

为什么它很重要?它意味着什么?列表是最流行的数据结构之一,它们的使用方式极大地影响着每个应用程序的性能。此外,CPython 是最流行和使用的实现,因此了解其内部实现细节至关重要。

具体来说,Python 中的列表是对其他对象的连续引用数组。指向此数组的指针和长度存储在列表头结构中。这意味着每次添加或删除项时,都需要调整引用数组的大小(重新分配)。幸运的是,在 Python 中,这些数组是以指数形式过度分配创建的,因此并非每个操作都需要调整大小。这就是添加和弹出元素的摊余成本如何在复杂性方面降低的原因。不幸的是,其他一些在普通链表中被认为“便宜”的操作在 Python 中的计算复杂度相对较高:

  • 使用list.insert方法在任意位置插入项目复杂性 O(n)
  • 使用list.deletedel删除项目-复杂性 O(n)

这里,n是列表的长度。至少,使用索引检索或设置元素是一项成本与列表大小无关的操作。以下是大多数列表操作的平均时间复杂性的完整表格:

|

活动

|

复杂性

| | --- | --- | | 复制 | O(n) | | 追加 | O(1) | | 插入 | O(n) | | 获取项目 | O(1) | | 删除项目 | O(n) | | 迭代 | O(n) | | 获取长度为k的切片 | O(k) | | Del 切片 | O(n) | | 设置长度为k的切片 | O(k+n) | | 延伸 | O(k) | | 乘以k | O(nk) | | 测试存在性(element in list) | O(n) | | min()/max() | O(n) | | 获取长度 | O(1) |

对于需要实际链表的情况(或者简单地说,在复杂度为 O(1)的情况下,每侧都有appendspop的数据结构),Python 在collections内置模块中提供deque。这是堆栈和队列的泛化,在需要双链接列表的任何地方都可以正常工作。

列表理解

正如您可能所知,编写这样一段代码是很痛苦的:

>>> 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]

除此之外,这种写作更有效,它更简短,涉及的元素更少。在更大的程序中,这意味着更少的 bug 和代码更容易阅读和理解。

提示

列表理解和内部数组调整

在一些 Python 程序员中有一个误区,即列表理解可以作为一种解决方法,因为表示列表对象的内部数组必须每隔几次添加就调整一次大小。有人说,数组将在适当的大小中分配一次。不幸的是,这不是真的。

解释器在理解评估期间无法知道结果容器的大小,也无法为其预先分配数组的最终大小。因此,内部阵列以与for循环相同的模式重新分配。尽管如此,在许多情况下,使用理解创建列表比使用普通循环更干净、更快。

其他成语

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

当需要逐个聚合多个列表(或任意 ITerable)的元素时,可以使用内置的zip()功能。这是在两个相同大小的 ITerable 上进行统一迭代的一种非常常见的模式:

>>> 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)

另一个流行的语法元素是序列解包。它不仅限于列表和元组,而且可以用于任何序列类型(甚至字符串和字节序列)。它允许您将元素序列解包到另一组变量中,只要赋值运算符左侧的变量数量与序列中的元素数量相同:

>>> first, second, third = "foo", "bar", 100
>>> first
'foo'
>>> second
'bar'
>>> third
100

解包还允许您使用带星号的表达式捕获单个变量中的多个元素,只要可以对其进行明确的解释。也可以对嵌套的序列执行解包。这非常有用,尤其是在迭代一些由序列构建的复杂数据结构时。以下是一些更复杂的解包示例:

>>> # starred expression to capture rest of the sequence
>>> first, second, *rest = 0, 1, 2, 3
>>> first
0
>>> second
1
>>> rest
[2, 3]

>>> # starred expression to capture middle of the sequence
>>> first, *inner, last = 0, 1, 2, 3
>>> first
0
>>> inner
[1, 2]
>>> last
3

>>> # nested unpacking
>>> (a, b), (c, d) = (1, 2), (3, 4)
>>> a, b, c, d
(1, 2, 3, 4)

字典

字典是 Python 中最通用的数据结构之一。dict允许将一组唯一键映射到如下值:

{
    1: ' one',
    2: ' two',
    3: ' three',
}

字典文字是一个非常基本的东西,你应该已经知道了。无论如何,Python 允许程序员也使用类似于前面提到的列表理解的理解创建一个新词典。下面是一个非常简单的例子:

squares = {number: number**2 for number in range(100)}

重要的是,使用列表理解的好处同样适用于词典理解。因此,在许多情况下,它们更高效、更短、更清洁。对于更复杂的代码,当创建字典需要许多if语句或函数调用时,简单的for循环可能是更好的选择,特别是如果它提高了可读性的话。

对于刚刚接触 Python3 的 Python 程序员来说,有一个关于迭代字典元素的重要注意事项。字典方法:keys()values()items()不再将列表作为其返回值类型。此外,Python 3 中缺少返回迭代器的对应项iterkeys()itervalues()iteritems()。相反,现在返回的keys()values()items()是视图对象:

  • keys():返回提供字典所有键视图的dict_keys对象
  • values():返回提供字典所有值视图的dict_values对象
  • items():返回提供字典所有(key, value)两元组视图的dict_items对象

视图对象以动态方式提供字典内容的视图,因此每次字典更改时,视图都会反映这些更改,如本例所示:

>>> words = {'foo': 'bar', 'fizz': 'bazz'}
>>> items = words.items()
>>> words['spam'] = 'eggs'
>>> items
dict_items([('spam', 'eggs'), ('fizz', 'bazz'), ('foo', 'bar')])

视图对象将旧方法实现返回的列表行为与其“iter”对应项返回的迭代器连接起来。视图不需要冗余地将所有值存储在内存中(就像列表一样),但仍然允许获取它们的长度(使用len和测试成员资格(使用in子句)。当然,观点是不可接受的。

最后一点很重要,keys()values()方法返回的两个视图确保键和值的顺序相同。在 Python2 中,如果希望确保检索到的键和值的顺序相同,则无法修改这两个调用之间的字典内容。dict_keysdict_values现在是动态的,因此即使字典的内容在keys()values()调用之间发生变化,迭代的顺序在这两个视图之间是一致的。

实施细则

CPython 使用带有伪随机探测的哈希表作为字典的底层数据结构。这似乎是一个非常深入的实现细节,但在不久的将来它不太可能改变,因此对程序员来说也是一个非常有趣的事实。

由于这个实现的细节,只有可散列的对象才能用作字典键。如果一个对象的哈希值在其生存期内从未改变,并且可以与不同的对象进行比较,那么该对象是可哈希的。每一个 Python 的内置类型都是不可变的,也是可散列的。列表、字典和集合等可变类型是不可散列的,因此它们不能用作字典键。定义类型是否可哈希的协议由两个方法组成:

  • __hash__:提供内部dict实现所需的散列值(作为整数)。对于作为用户定义类实例的对象,它是从它们的id()派生的。
  • __eq__:比较两个值相同的对象。默认情况下,作为用户定义类实例的所有对象都比较不相等,但它们自己除外。

比较相等的两个对象必须具有相同的哈希值。反之亦然。这意味着哈希可能发生冲突。具有相同哈希的两个对象可能不相等。这是允许的,并且每个 Python 实现都必须能够解决哈希冲突。CPython 使用开放寻址来解决此类冲突(https://en.wikipedia.org/wiki/Open_addressing )。尽管如此,冲突的概率极大地影响性能,如果冲突的概率很高,字典将无法从其内部优化中获益。

虽然添加、获取和删除项这三种基本操作的平均时间复杂度等于 O(1),但它们的摊销最坏情况复杂度要高得多,即 O(n),其中n是当前字典的大小。此外,如果将用户定义的类对象用作字典键,并且对它们进行了不正确的哈希处理(具有很高的冲突风险),那么这将对字典性能产生巨大的负面影响。CPyhton 字典的时间复杂性完整表如下:

|

活动

|

平均复杂度

|

摊销最坏情况复杂性

| | --- | --- | --- | | 获取项目 | O(1) | O(n) | | 固定项目 | O(1) | O(n) | | 删除项目 | O(1) | O(n) | | 复制 | O(n) | O(n) | | 迭代 | O(n) | O(n) |

同样重要的是要知道,在复制和迭代字典的最坏情况复杂度中的n数是字典曾经达到的最大大小,而不是当前项目计数。换言之,反复阅读曾经庞大但时间大大缩短的词典可能需要惊人的长时间。因此,在某些情况下,如果必须经常对新的 dictionary 对象进行迭代,则最好创建一个新的 dictionary 对象,而不只是从以前的 dictionary 对象中删除元素。

弱点和备选方案

使用字典的一个常见缺陷是它们不能保留添加新键的元素顺序。在某些情况下,当字典键使用哈希值也是连续值的连续键(例如,使用整数)时,由于字典的内部实现,结果顺序可能相同:

>>> {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的有序字典。它可以选择接受 iterable 作为初始化参数:

>>> from collections import OrderedDict
>>> OrderedDict((str(number), None) for number in range(5)).keys()
odict_keys(['0', '1', '2', '3', '4'])

它还具有一些额外的功能,例如使用popitem()方法从两端弹出项目,或者使用move_to_end()方法将指定元素移动到一端。Python 文档中有关于该集合的完整参考资料(请参阅https://docs.python.org/3/library/collections.html )。

另一个重要的注意事项是,在非常古老的代码库中,dict可以用作确保元素唯一性的原始集实现。虽然这将给出适当的结果,但除非针对低于 2.3 的 Python 版本,否则应该忽略这一点。以这种方式使用词典在资源方面是浪费的。Python 有一个内置的set类型来实现这一目的。事实上,它的内部实现与 CPython 中的字典非常相似,但提供了一些附加功能以及特定的集合相关优化。

集合是一种非常健壮的数据结构,在元素顺序不如元素的唯一性和测试效率重要的情况下非常有用,如果元素包含在集合中。它们与类似的数学概念非常相似。集合以两种风格的内置类型提供:

  • set():这是唯一、不可变(可散列)对象的可变、非有序、有限集合
  • frozenset():这是一个不可变、可散列、非有序的唯一、不可变(可散列)对象集合

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})})

可通过三种方式创建可变集:

  • 使用接受可选 iterable 作为初始化参数的set()调用,例如set([0, 1, 2])
  • 使用集合理解,如{element for element in range(3)}
  • 使用集合文字,如{1, 2, 3}

请注意,对集合使用文字和理解需要格外小心,因为它们在形式上与词典文字和理解非常相似。此外,空集对象没有文本。空的花括号{}是为空字典文本保留的。

实施细则

CPython 中的集合与字典非常相似。事实上,它们的实现类似于带有伪值的字典,其中只有键是实际的集合元素。此外,集合利用映射中缺少的值进行额外优化。

由于这一点,集合允许非常快速的添加、删除和检查元素是否存在,平均时间复杂度等于 O(1)。尽管如此,由于 CPython 中集合的实现依赖于类似的哈希表结构,因此这些操作的最坏情况复杂性为 O(n),其中n是集合的当前大小。

其他实施细节也适用。要包含在集合中的项必须是可散列的,如果集合中用户定义类的实例散列不好,这将对性能产生负面影响。

超出基本集合–集合模块

每种数据结构都有其缺点。没有一个集合可以解决所有问题,其中的四种基本类型(元组、列表、集合和字典)仍然没有广泛的选择。这些是具有专用文字语法的最基本和最重要的集合。幸运的是,Python 通过collections内置模块在其标准库中提供了更多选项。其中一个已经提到(deque。以下是本模块提供的最重要的收藏:

  • namedtuple():这是一个工厂函数,用于创建元组子类,其索引可以作为命名属性访问
  • deque:这是一个双端队列,是堆栈和队列的列表式泛化,两端都有快速的追加和弹出
  • ChainMap:这是一个类似字典的类,用于创建多个映射的单个视图
  • Counter:这是一个用于计算哈希对象的字典子类
  • OrderedDict:这是一个字典子类,它保留了条目添加的顺序
  • defaultdict:这是一个 dictionary 子类,它可以用提供的默认值提供缺少的值

第 12 章优化–一些强大的技术中提供了有关从集合模块中选择的集合的更多详细信息以及关于在哪里值得使用它们的一些建议。

高级语法

很难客观地判断语言语法的哪一部分是高级的。对于本章关于高级语法元素的目的,我们将考虑与任何特定的内置数据类型不直接相关的元素,这些元素在开始时比较难以掌握。最常见的 Python 特性可能很难理解:

  • 遍历器
  • 发电机
  • 装饰师
  • 上下文管理器

迭代器

迭代器只不过是实现迭代器协议的容器对象。它基于两种方法:

  • __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

迭代器本身是一个低层次的特性和概念,没有迭代器,程序就可以生存。但它们为更有趣的特性生成器提供了基础。

收益率报表

生成器提供了一种优雅的方式,为返回元素序列的函数编写简单高效的代码。基于yield语句,它们允许您暂停函数并返回中间结果。该函数保存其执行上下文,如有必要,可以稍后继续。

例如,斐波那契数列可以用迭代器编写(这是关于迭代器的 PEP 中提供的示例):

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模块从文本流中生成令牌,并为每个经过处理的行返回一个iterator,该行可以传递给某些处理:

>>> 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 hello_world():\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 中关于generators的另一个重要特性是能够与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 章并发中介绍。

装饰师

Python 中添加了装饰器,以使函数和方法包装(接收函数并返回增强函数的函数)更易于阅读和理解。最初的用例是能够将方法定义为类方法或静态方法。如果没有 decorator 语法,它将需要一个相当稀疏和重复的定义:

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)

如果 decorator 语法用于相同目的,则代码会更短,更容易理解:

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

    @classmethod
    def some_class_method(cls):
        print("this is class method")

通用语法和可能的实现

decorator 通常是一个命名对象(lambda表达式不允许使用),它在被调用时接受一个参数(它将是修饰函数)并返回另一个可调用对象。这里使用“Callable”而不是有预谋的“function”。虽然装饰器经常在方法和函数的范围内讨论,但它们并不限于此。事实上,任何可调用的对象(实现__call__方法的任何对象都被认为是可调用的)都可以用作装饰器,并且它们返回的对象通常不是简单的函数,而是实现自身__call__方法的更复杂类的更多实例。

decorator 语法只是一种语法糖。请考虑下列装饰器使用情况:

@some_decorator
def decorated_function():
    pass

这通常可以由显式的装饰器调用和函数重新分配来代替:

def decorated_function():
    pass
decorated_function = some_decorator(decorated_function)

但是,如果在单个函数上使用多个装饰符,则后者可读性较差,并且也很难理解。

提示

装饰师甚至不需要返回可调用的!

事实上,任何函数都可以用作 decorator,因为 Python 不强制 decorator 的返回类型。因此,使用某个函数作为接受单个参数但不返回 callable 的修饰符,比如说str,在语法上是完全有效的。如果用户试图调用以这种方式装饰的对象,则最终会失败。无论如何,decorator 语法的这一部分为一些有趣的实验创建了一个字段。

作为一项功能

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

通用模式如下所示:

def mydecorator(function):
    def wrapped(*args, **kwargs):     
        # do some stuff before the original
        # function gets called
        result = function(*args, **kwargs)
        # do some stuff after function call and
        # return the result
        return result
    # return wrapper as a decorated function
    return wrapped

作为一个班级

虽然装饰器几乎总是可以使用函数实现,但在某些情况下,使用用户定义的类是更好的选择。当装饰器需要复杂的参数化或依赖于特定的状态时,这通常是正确的。

非参数化装饰器作为类的通用模式如下所示:

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

    def __call__(self, *args, **kwargs):
        # do some stuff before the original
        # function gets called
        result = self.function(*args, **kwargs)
        # do some stuff after function call and
        # return the result
        return result

参数化装饰器

在实际代码中,经常需要使用可以参数化的装饰器。当函数用作装饰器时,解决方案很简单——必须使用第二级包装。下面是一个简单的 decorator 示例,它在每次调用修饰函数时重复执行指定次数的修饰函数:

def repeat(number=3):
    """Cause decorated function to be repeated a number of times.

    Last value of original function call is returned as a result
    :param number: number of repetitions, 3 if not specified
    """
    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'

内省保存装饰师

使用 decorator 的常见缺陷是在使用 decorator 时没有保留函数元数据(主要是 docstring 和原始名称)。前面所有的例子都有这个问题。他们通过组合创建了一个新函数,并返回了一个新对象,而不考虑原始对象的标识。这使得调试以这种方式修饰的函数变得更加困难,并且还会破坏大多数可能使用的自动文档工具,因为原始的 docstring 和函数签名不再可访问。

但让我们来详细了解一下。假设我们有一个虚拟装饰器,它只做装饰和其他一些用它装饰的功能:

def dummy_decorator(function):
    def wrapped(*args, **kwargs):
        """Internal wrapped function documentation."""
        return function(*args, **kwargs)
    return wrapped

@dummy_decorator
def function_with_important_docstring():
    """This is important docstring we do not want to lose."""

如果我们在 Python 交互会话中检查function_with_important_docstring(),我们会注意到它丢失了原始名称和 docstring:

>>> function_with_important_docstring.__name__
'wrapped'
>>> function_with_important_docstring.__doc__
'Internal wrapped function documentation.'

此问题的正确解决方案是使用functools模块提供的内置wraps()装饰器:

from functools import wraps

def preserving_decorator(function):
    @wraps(function)
    def wrapped(*args, **kwargs):
        """Internal wrapped function documentation."""
        return function(*args, **kwargs)
    return wrapped

@preserving_decorator
def function_with_important_docstring():
    """This is important docstring we do not want to lose."""

以这种方式定义 decorator 后,将保留重要的函数元数据:

>>> function_with_important_docstring.__name__
'function_with_important_docstring.'
>>> function_with_important_docstring.__doc__
'This is important docstring we do not want to lose.'

用法和有用示例

由于装饰器是在第一次读取模块时由解释器加载的,因此它们的使用应限于可以一般应用的包装器。如果 decorator 绑定到方法的类或它增强的函数的签名,那么应该将其重构为一个常规的可调用函数,以避免复杂性。在任何情况下,当装饰程序处理 API 时,一个好的实践是将它们分组到一个易于维护的模块中。

装饰器的常见模式有:

  • 参数检查
  • 缓存
  • 代理
  • 上下文提供程序

参数检查

在特定上下文中执行函数时,检查函数接收或返回的参数非常有用。例如,如果要通过 XML-RPC 调用函数,Python 将无法像静态类型语言那样直接提供其完整签名。当 XML-RPC 客户端请求函数签名时,需要此功能来提供内省功能。

提示

XML-RPC 协议

XML-RPC 协议是一种轻量级远程过程调用协议,它使用 HTTP 上的 XML 对其调用进行编码。对于简单的客户机-服务器交换,它通常被用来代替 SOAP。与 SOAP 不同,SOAP 提供了一个列出所有可调用函数(WSDL)的页面,XML-RPC 没有可用函数的目录。提出了一个允许发现服务器 API 的协议扩展,Python 的xmlrpc模块实现了它(参见https://docs.python.org/3/library/xmlrpc.server.html )。

自定义装饰器可以提供这种类型的签名。它还可以确保进出的符合定义的签名参数:

rpc_info = {}

def xmlrpc(in_=(), out=(type(None),)):
    def _xmlrpc(function):
        # registering the signature
        func_name = function.__name__
        rpc_info[func_name] = (in_, out)
        def _check_types(elements, types):
            """Subfunction that checks the 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))

        # wrapped function
        def __xmlrpc(*args):  # no keywords allowed
            # checking what goes in
            checkable_args = args[1:]  # removing self
            _check_types(checkable_args, in_)
            # running the function
            res = function(*args)
            # checking what goes out
            if not type(res) in (tuple, list):
                checkable_res = (res,)
            else:
                checkable_res = res
            _check_types(checkable_res, out)

            # the function and the type
            # checking succeeded
            return res
        return __xmlrpc
    return _xmlrpc

decorator 将函数注册到一个全局字典中,并为其参数和返回值保留一个类型列表。请注意,该示例经过了高度简化,以演示参数检查装饰器。

的使用示例如下:

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'>), (<class 'NoneType'>,))}
>>> 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'>

缓存

缓存修饰符与参数检查非常相似,但主要关注那些内部状态不影响输出的函数。每一组参数都可以链接到一个唯一的结果。这种编程风格是函数式编程(参见的特点 http://en.wikipedia.org/wiki/Functional_programming ),可在输入值集有限时使用。

因此,缓存装饰器可以将输出与计算它所需的参数保持在一起,并在后续调用中直接返回它。这种行为称为记忆(参见http://en.wikipedia.org/wiki/Memoizing ),作为一名装饰师很容易实现:

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)

            # do we have it already ?
            if (key in cache and
                not is_obsolete(cache[key], duration)):
                print('we got a winner')
                return cache[key]['value']

            # computing
            result = function(*args, **kw)
            # storing the result
            cache[key] = {
                'value': result,
                'time': time.time()
            }
            return result
        return __memoize
    return _memoize

使用有序参数值构建SHA散列键,结果存储在全局字典中。哈希是使用 pickle 生成的,pickle 是冻结作为参数传递的所有对象的状态的一种快捷方式,确保所有参数都是好的候选参数。例如,如果使用线程或套接字作为参数,则会出现PicklingError。(参见https://docs.python.org/3/library/pickle.htmlduration参数是用于在自上次函数调用以来经过太多时间时使缓存的值无效。

下面是一个用法示例:

>>> @memoize()
... def very_very_very_complex_stuff(a, b):
...     # if your computer gets too hot on this calculation
...     # consider stopping it
...     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) # invalidates the cache after 1 second
... 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 章优化——一些强大的技术提供了缓存的详细信息和技术。

代理

代理修饰符用于使用全局机制标记和注册函数。例如,根据当前用户的不同,保护代码访问的安全层可以使用具有可调用用户所需的相关权限的集中式检查器来实现:

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

上下文提供程序

上下文装饰器确保函数可以在正确的上下文中运行,或者在函数前后运行一些代码。换句话说,它设置和取消设置特定的执行环境。例如,当一个数据项必须在多个线程之间共享时,必须使用一个锁来确保它不受多个访问的保护。此锁可以在装饰器中编码,如下所示:

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():  # make sure it locks the resource
    pass

上下文装饰符更经常地被上下文管理器的使用所取代with语句也将在本章后面介绍。

上下文管理器–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:
    # block of code
    ...

此外,如果上下文管理器提供上下文变量,则可以使用as子句将其存储在本地:

with context_manager as context:
    # block of code
    ...

请注意,可以同时使用多个上下文管理器,如下所示:

with A() as a, B() as b:
    ...

这相当于嵌套它们,如下所示:

with A() as a:
    with B() as b:
        ...

作为一个班级

任何实现上下文管理器协议的对象都可以用作上下文管理器。本协议包括两种特殊方法:

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

  1. 调用了__enter__方法。任何返回值都绑定到指定的 as 子句的目标。
  2. 执行内部代码块。
  3. 调用了__exit__方法。

__exit__接收三个参数,当代码块内发生错误时,这些参数被填充。如果没有发生错误,则所有三个参数都设置为None。发生错误时,__exit__不应重新引发错误,因为这是调用方的责任。但它可以通过返回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'

作为一项功能–contextlib 模块

使用类似乎是实现 Python 语言中提供的任何协议的最灵活的方式,但对于许多用例来说,可能太多了。标准库中添加了一个contextlib模块,以提供可与上下文管理器一起使用的助手。其中最有用的部分是contextmanager装饰器。它允许您在单个函数中同时提供__enter____exit__部分,由yield语句分隔(注意,这使函数成为生成器)。使用此装饰器编写的上一个示例类似于以下代码:

from contextlib import contextmanager

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

    try:
        yield
    except Exception as e:
        print('leaving context')
        print('with an error (%s)' % e)
        # exception needs to be reraised
        raise
    else:
        print('leaving context')
        print('with no error')

如果发生任何异常,函数需要重新引发它以传递它。请注意,context_illustration如果需要,可以有一些参数,只要它们在调用中提供。这个小助手简化了普通的基于类的上下文 API,就像生成器使用基于类的迭代器 API 一样。

本模块提供的其他三名助手为:

  • closing(element):返回在退出时调用元素 close 方法的上下文管理器。例如,这对于处理流的类很有用。
  • supress(*exceptions):如果指定的异常出现在 with 语句的主体中,则此选项将抑制这些异常。
  • redirect_stdout(new_target)redirect_stderr(new_target):将块内任何代码的sys.stdoutsys.stderr输出重定向到类似文件对象的另一个文件。

您可能还不知道的其他语法元素

Python 语法中有一些元素并不流行,也很少使用。这是因为它们要么提供很少的收益,要么它们的用法很难记忆。因此,许多 Python 程序员(即使有多年的经验)根本不知道它们的存在。这些特征最显著的例子如下:

  • 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

这在某些情况下很方便,因为它有助于删除一些“哨兵”变量,如果用户希望在发生break时存储信息,则可能需要这些变量。这使得代码更简洁,但可能会让不熟悉此类语法的程序员感到困惑。有人说else子句的这种含义是违反直觉的,但这里有一个简单的提示,可以帮助你记住它是如何工作的。记住for循环后面的else子句只是表示“不中断”。

功能注释

函数注释是 Python 3 最独特的特性之一。官方文档指出,注释是关于用户定义函数所使用类型的完全可选元数据信息,但事实上,它们并不限于类型暗示,Python 及其标准库中也没有利用此类注释的单一功能。这就是为什么这个功能是独特的,它没有任何语法意义。注释可以简单地为函数定义,并且可以在运行时检索,但仅此而已。如何处理它们留给开发人员。

一般语法

Python 文档中的一个稍加修改的示例最好地展示了如何定义和检索函数注释:

>>> def f(ham: str, eggs: str = 'eggs') -> str:
...     pass
... 
>>> print(f.__annotations__)
{'return': <class 'str'>, 'eggs': <class 'str'>, 'ham': <class 'str'>}

如前所述,参数注释是由表达式定义的,该表达式计算注释的值,后跟冒号。返回注释由表示def语句结尾的冒号和参数列表后面的文字->之间的表达式定义。

一旦定义,注释在函数对象的__annotations__属性中作为字典可用,并且可以在应用程序运行时检索。

任何表达式都可以用作注释,并且位于默认参数附近,这一事实允许创建一些混乱的函数定义,如下所示:

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

然而,注释的这种使用除了混淆之外没有其他用途,即使没有注释,编写难以阅读和维护的代码也相对容易。

可能的用途

尽管注释有很大的潜力,但它们并未得到广泛应用。一篇解释 Python 3 新增功能的文章(参考https://docs.python.org/3/whatsnew/3.0.html 表示,此功能的目的是“鼓励通过元类、装饰器或框架进行实验”。另一方面,正式提出的功能注释PEP 3107列出了以下一组可能的用例:

  • 提供打字信息
    • 类型检查
    • 让 IDE 显示函数期望和返回的类型
    • 函数重载/泛型函数
    • 外语桥
    • 改编本
    • 谓词逻辑函数
    • 数据库查询映射
    • RPC 参数封送
  • 其他资料
    • 参数和返回值的文档

尽管函数注释与 Python3 一样古老,但仍然很难找到任何流行的、主动维护的包将其用于类型检查之外的其他用途。因此,函数注释基本上还是好的,只是为了实验和发挥最初的作用,为什么它们被包含在 Python 3 的初始版本中。

总结

本章介绍了与 Python 类和面向对象编程没有直接关系的各种最佳语法实践。本章的第一部分专门讨论 Python 序列和集合的语法特性,还讨论了字符串和字节相关序列。本章的其余部分介绍了两组独立的语法元素,一组是初学者相对难以理解的(如迭代器、生成器和装饰器),另一组是不太为人所知的(for…else子句和函数注释)。