Skip to content

Latest commit

 

History

History
973 lines (663 loc) · 33.2 KB

File metadata and controls

973 lines (663 loc) · 33.2 KB

七、推导式、可迭代对象和生成器

对象序列的抽象概念在编程中无处不在。它可以被用来建模诸如简单字符串、复杂对象列表和无限长的传感器输出流等广泛不同的概念。了解到 Python 包含一些非常强大和优雅的用于处理序列的工具,您可能不会感到惊讶。事实上,Python 对创建和操作序列的支持是该语言的亮点之一。

在本章中,我们将介绍 Python 为处理序列提供的三个关键工具:理解、可编辑和生成器。理解包含一个专用语法,用于以声明方式创建各种类型的序列。Iterables迭代协议构成 Python 中序列和迭代的核心抽象和 API;它们允许您定义新的序列类型,并对迭代进行细粒度控制。最后,生成器允许我们强制定义惰性序列,这在许多情况下都是一种非常强大的技术。

让我们直接进入理解。

理解

Python 中的理解是一种简洁的语法,用于以声明式或函数式的方式描述列表、集合或词典。这种速记具有可读性和表达性,这意味着理解在向人类读者传达意图方面非常有效。有些理解几乎读起来像自然语言,使它们能够很好地自我记录。

列表解析

如上所述,列表推导式是创建列表的简捷方法。这是一个使用简洁语法的表达式,描述了如何定义列表元素。理解要比解释容易得多,所以让我们来介绍一个 Python REPL。首先,我们将通过拆分字符串来创建单词列表:

>>> words = "If there is hope it lies in the proles".split()
>>> words
['If', 'there', 'is', 'hope', 'it', 'lies', 'in', 'the', 'proles'] 

现在是列表推导式。理解包含在方括号中,就像文字列表一样,但它不包含文字元素,而是包含一段描述如何构造列表元素的声明性代码:

>>> [len(word) for word in words]
[2, 5, 2, 4, 2, 4, 2, 3, 6]

在这里,新列表是通过依次将名称word绑定到words中的每个值,然后计算len(word)以在新列表中创建相应的值而形成的。换句话说,这将构造一个新列表,其中包含单词中字符串的长度;很难想象有更有效的方式来表达这个新列表!

列表推导式语法

列表推导式的一般形式为:

[ expr(item) for item in iterable ]

也就是说,对于右侧iterable中的每个项目,我们计算左侧的表达式expr(item)(几乎总是,但不一定是,根据项目)。我们将该表达式的结果用作正在构造的列表的下一个元素。

上述理解相当于以下命令式代码的声明:

>>> lengths = []
>>> for word in words:
...     lengths.append(len(word))
...
>>> lengths
[2, 5, 2, 4, 2, 4, 2, 3, 6]

列表的元素

注意,我们在列表推导式中迭代的source对象不需要是列表本身。它可以是实现 iterable 协议的任何对象,例如元组。

理解的表达式部分可以是任何 Python 表达式。在这里,我们使用range()(一个iterable对象)来生成源序列,从而找到前 20 个阶乘中每个阶乘的十进制位数:

>>> from math import factorial
>>> f = [len(str(factorial(x))) for x in range(20)]
>>> f
[1, 1, 1, 1, 2, 3, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 18]

还要注意的是,列表推导式产生的对象类型不超过或少于常规列表:

>>> type(f)
<class 'list'>

重要的是要记住这一事实,因为我们看其他类型的理解,并考虑如何执行无限序列上的迭代。

集合理解

集合支持类似的理解语法,如您所料,使用大括号。我们以前的“阶乘中的位数”结果包含重复项,但通过构建集合而不是列表,我们可以消除它们:

>>> s = {len(str(factorial(x))) for x in range(20)}
>>> s
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 18}

与列表推导式一样,集合理解生成标准set对象:

>>> type(s)
<class 'set'>

注意,由于集合是无序容器,因此结果集合不一定以有意义的顺序存储。

字典推导

第三种理解是词典理解。与集合理解语法一样,字典理解也使用大括号。与集合理解不同的是,我们现在提供了两个冒号分隔的表达式——第一个用于键,第二个用于值——将对结果字典中的每个新项进行串联计算。这是一本我们可以使用的字典:

>>> country_to_capital = { 'United Kingdom': 'London',
...                        'Brazil': 'Brasília',
...                        'Morocco': 'Rabat',
...                        'Sweden': 'Stockholm' }

字典理解的一个很好的用途是反转字典,这样我们可以在相反的方向执行有效的查找:

>>> capital_to_country = {capital: country for country, capital in 
                          country_to_capital.items()}
>>> from pprint import pprint as pp
>>> pp(capital_to_country)
{'Brasília': 'Brazil',
 'London': 'United Kingdom',
 'Rabat': 'Morocco',
 'Stockholm': 'Sweden'}

Note The  dictionary comprehensions do not operate directly on dictionary sources! If we want both keys and values from a source dictionary, then we should use the items() method coupled with tuple unpacking to access the keys and values separately.

如果你的理解产生了一些相同的键,后面的键将覆盖前面的键。在本例中,我们将单词的前几个字母映射到单词本身,但只保留最后一个h-单词:

>>> words = ["hi", "hello", "foxtrot", "hotel"]
>>> { x[0]: x for x in words }
{'h': 'hotel', 'f': 'foxtrot'}

理解复杂性

记住,你可以在任何理解中使用的表达的复杂性是没有限制的。不过,为了您的程序员同事着想,您应该避免走极端。相反,将复杂表达式提取到单独的函数中以保持可读性。以下内容接近于词典理解的合理限度:

>>> import os
>>> import glob
>>> file_sizes = {os.path.realpath(p): os.stat(p).st_size for p in 
                  glob.glob('*.py')}
>>> pp(file_sizes)
{'/Users/pyfund/examples/exceptional.py': 400,
 '/Users/pyfund/examples/keypress.py': 778,
 '/Users/pyfund/examples/scopes.py': 133,
 '/Users/pyfund/examples/words.py': 1185}

这将使用glob模块查找目录中的所有 Python 源文件。然后,它会创建一个字典,其中包含从这些文件到文件大小的路径。

过滤理解

所有三种类型的集合理解都支持一个可选的筛选子句,该子句允许我们选择由左边的表达式计算源的哪些项。过滤子句是通过在理解的序列定义之后添加if <boolean expression>来指定的,如果布尔表达式对输入序列中的某个项返回 false,则在结果中不对该项求值。

为了使这一点更有趣,我们将首先定义一个函数来确定其输入是否为素数:

>>> from math import sqrt
>>> def is_prime(x):
...     if x < 2:
...         return False
...     for i in range(2, int(sqrt(x)) + 1):
...         if x % i == 0:
...             return False
...     return True
...

我们现在可以在列表推导式的过滤子句中使用它来生成小于100的所有素数:

>>> [x for x in range(101) if is_prime(x)]
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]

滤波与变换相结合

这里的x构造有点奇怪,因为我们没有对过滤后的值进行任何转换;x中的表达式只是x本身。然而,没有什么可以阻止我们将过滤谓词与转换表达式相结合。下面是一个字典理解,它将正好有三个除数的数字映射到这些除数的元组:

>>> prime_square_divisors = {x*x:(1, x, x*x) for x in range(101) if 
                             is_prime(x)}
>>> pp(prime_square_divisors)
{4: (1, 2, 4),
 9: (1, 3, 9),
 25: (1, 5, 25),
 49: (1, 7, 49),
 121: (1, 11, 121),
 169: (1, 13, 169),
 289: (1, 17, 289),
 361: (1, 19, 361),
 529: (1, 23, 529),
 841: (1, 29, 841),
 961: (1, 31, 961),
 1369: (1, 37, 1369),
 1681: (1, 41, 1681),
 1849: (1, 43, 1849),
 2209: (1, 47, 2209),
 2809: (1, 53, 2809),
 3481: (1, 59, 3481),
 3721: (1, 61, 3721),
 4489: (1, 67, 4489),
 5041: (1, 71, 5041),
 5329: (1, 73, 5329),
 6241: (1, 79, 6241),
 6889: (1, 83, 6889),
 7921: (1, 89, 7921),
 9409: (1, 97, 9409)}

禅宗时刻

禅宗时刻:代码只写了一次,但却被反复阅读。越少越好:

Figure 7.1: Moment of Zen

理解往往比其他方法更具可读性。然而,过度使用理解也是可能的。有时,一个较长或复杂的理解可能比同等的for循环更难理解。关于什么时候应该首选一种形式,没有硬性规定,但是在编写代码时要认真,尽量根据您的情况选择最佳形式。

最重要的是,你的理解应该是纯粹的功能性的——也就是说,它们应该没有副作用。如果您需要创建副作用,例如在迭代过程中打印到控制台,请使用另一个构造,例如for-循环。

迭代协议

理解和for-循环是执行迭代最常用的语言特性。两者都从一个来源一个接一个地获取项目,然后依次对每个项目进行处理。然而,默认情况下,理解和for循环都会在整个序列上迭代,而有时需要更细粒度的控制。在本节中,我们将通过研究构建大量 Python 语言行为的两个重要概念来了解如何使用这种细粒度控制:iterable对象和iterator对象,它们都反映在标准 Python 协议中。

iterable 协议定义了iterable对象必须实现的 API。也就是说,如果您希望能够使用for-循环或理解来迭代对象,那么该对象必须实现 iterable 协议。像 list 这样的内置类实现了 iterable 协议。您可以将实现 iterable 协议的对象传递给内置的iter()函数,以获取iterable对象的迭代器

迭代器支持迭代器协议。该协议要求我们可以将iterator对象传递给内置next()函数,以从基础集合中获取下一个值。

迭代协议的一个例子

与往常一样,在 Python REPL 上的演示将帮助所有这些概念具体化为您可以使用的东西。我们首先列出季节名称作为我们的iterable目标:

>>> iterable = ['Spring', 'Summer', 'Autumn', 'Winter']

然后,我们要求iterable对象使用iter()内置函数为我们提供一个迭代器:

>>> iterator = iter(iterable)

接下来,我们使用内置的next()iterator对象请求一个值:

>>> next(iterator)
'Spring'

每次调用next()都会在序列中移动迭代器:

>>> next(iterator)
'Summer'
>>> next(iterator)
'Autumn'
>>> next(iterator)
'Winter'

但是当我们到达终点时会发生什么呢?

>>> next(iterator)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
StopIteration

在自由主义的壮观展示中,Python 提出了一个例外。对于那些来自其他编程语言、对异常采取更直接的处理方法的人来说,可能会觉得这有点离谱,但是,真的,还有什么比到达集合的末尾更离奇呢?毕竟它只有一个目的!

当考虑到 iterable 系列可能是一个潜在的无限数据流时,这种将 Python 语言设计决策合理化的尝试更有意义。在这种情况下,到达终点真的写回家,或者确实提出一个例外。

迭代协议的一个更实际的例子

随着for-循环和理解在我们指尖,这些低级迭代协议的效用可能并不明显。为了演示更具体的用法,这里有一个小实用函数,当传递一个iterable对象时,它返回该序列的第一项,或者,如果序列为空,则引发一个ValueError

>>> def first(iterable):
...     iterator = iter(iterable)
...     try:
...         return next(iterator)
...     except StopIteration:
...         raise ValueError("iterable is empty")
...

这在任何iterable对象上都能正常工作,在这种情况下,列表和集合都可以:

>>> first(["1st", "2nd", "3rd"])
'1st'
>>> first({"1st", "2nd", "3rd"})
'1st'
>>> first(set())
Traceback (most recent call last):
 File "./iterable.py", line 17, in first
 return next(iterator)
StopIteration

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "./iterable.py", line 19, in first
 raise ValueError("iterable is empty")
ValueError: iterable is empty

值得注意的是,更高级别的迭代构造,例如for-循环和理解,直接构建在这个较低级别的迭代协议之上。

生成函数

现在我们来谈谈生成器函数,这是 Python 编程语言最强大、最优雅的特性之一。Python 生成器提供了用函数中的代码描述 iterable 系列的方法。这些序列的评估是惰性的,这意味着它们只根据需要计算下一个值。这一重要特性使他们能够对无限的值序列进行建模,而没有明确的终点,例如来自传感器或活动日志文件的数据流。通过仔细设计生成器函数,我们可以制作通用的流处理元素,这些元素可以组合成复杂的管道。

收益率关键字

生成器由任何 Python 函数定义,该函数在其定义中至少使用一次yield关键字。它们也可能包含不带参数的return关键字,并且与任何其他函数一样,在定义的末尾有一个隐式返回。

为了理解生成器的作用,让我们从 Python REPL 的一个简单示例开始。让我们定义生成器,然后检查生成器是如何工作的。

生成器函数由def引入,与常规 Python 函数一样:

>>> def gen123():
...     yield 1
...     yield 2
...     yield 3
...

现在我们调用gen123()并将其返回值赋给g

>>> g = gen123()

正如您所看到的,gen123()的调用与任何其他 Python 函数一样。但它回来了什么?

>>> g
<generator object gen123 at 0x1006eb230>

生成器是迭代器

字母表g是一个generator对象。生成器实际上是 Python迭代器,因此我们可以使用迭代器协议从序列中检索或生成连续值:

>>> next(g)
1
>>> next(g)
2
>>> next(g)
3

现在我们已经从生成器中生成了最后一个值,请注意发生了什么。对next()的后续调用会引发StopIteration异常,就像其他 Python 迭代器一样:

>>> next(g)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
StopIteration

因为生成器是迭代器,而且迭代器也必须是可 iterable 的,所以它们可以用于所有需要iterable对象的常见 Python 构造中,例如for-循环:

>>> for v in gen123():
...     print(v)
...
1
2
3

请注意,对 generator 函数的每次调用都会返回一个新的generator对象:

>>> h = gen123()
>>> i = gen123()
>>> h
<generator object gen123 at 0x1006eb2d0>
>>> i
<generator object gen123 at 0x1006eb280>
>>> h is i
False

还要注意如何独立推进每个generator对象:

>>> next(h)
1
>>> next(h)
2
>>> next(i)
1

生成器代码何时执行?

让我们更仔细地看看,当我们的生成器函数的代码被执行时,它是多么重要,并且至关重要。为此,我们将创建一个稍微复杂一点的生成器,它使用良好的老式打印语句跟踪其执行:

>>> def gen246():
...     print("About to yield 2")
...     yield 2
...     print("About to yield 4")
...     yield 4
...     print("About to yield 6")
...     yield 6
...     print("About to return")
...
>>> g = gen246()

此时,generator对象已创建并返回,但生成器函数体中的所有代码尚未执行。我们先打个电话给next()

>>> next(g)
About to yield 2
2

请看,当我们请求第一个值时,生成器主体如何运行并包含第一个yield语句。代码执行的距离刚好足以产生下一个值。

>>> next(g)
About to yield 4
4

当我们从发电机请求下一个值时,发电机功能的执行在其停止点恢复,并继续运行,直到下一个产量:

>>> next(g)
About to yield 6
6

在最终值返回后,下一个请求导致生成器函数执行,直到它在函数体的末尾返回,这反过来引发预期的StopIteration异常。

>>> next(g)
About to return
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
StopIteration

现在,我们已经了解了生成器执行是如何通过调用next()启动并被yield语句中断的,我们可以继续在生成器函数体中放置更复杂的代码。

在生成器函数中保持显式状态

现在我们来看看生成器函数是如何在每次请求下一个值时恢复执行的,它可以维护局部变量中的状态。在这样做的过程中,我们的发电机将更加有趣和有用。我们将展示两个演示惰性计算的生成器,稍后我们将它们组合到生成器管道中。

第一个有状态生成器:take()

我们要看的第一个生成器是take(),它从序列的前端检索指定数量的元素:

def take(count, iterable):
 """Take items from the front of an iterable.

 Args:
 count: The maximum number of items to retrieve.
 iterable: The source of the items.

 Yields:
 At most 'count' items from 'iterable'.
 """
 counter = 0
 for item in iterable:
 if counter == count:
 return
 counter += 1
 yield item

Note That the function defines a generator because it contains at least one yield statement. This particular generator also contains a return statement to terminate the stream of yielded values. The generator simply uses a counter to keep track of how many elements have been yielded so far, returning when a request is made for any elements beyond that requested count.

由于生成器是惰性的,并且仅在请求时生成值,因此我们将使用run_take()函数中的for-循环来驱动执行:

def run_take():
 items = [2, 4, 6, 8, 10]
 for item in take(3, items):
 print(item)

在这里,我们创建一个名为 items 的源列表,并将其与计数3一起传递给生成器函数。在内部,for循环将使用迭代器协议从take()生成器检索值,直到终止。

第二个有状态生成器:distinct()

现在让我们把第二台发电机带到图片中。此生成器功能称为distinct(),通过跟踪在集合中已看到的元素,消除重复项:

def distinct(iterable):
 """Return unique items by eliminating duplicates.

 Args:
 iterable: The source of the items.

 Yields:
 Unique elements in order from 'iterable'.
 """
 seen = set()
 for item in iterable:
 if item in seen:
 continue
 yield item
 seen.add(item)

在这个生成器中,我们还使用了一个我们以前没有见过的控制流构造:continue关键字。continue语句完成循环的当前迭代,并立即开始下一次迭代。在本例中执行时,执行将被传输回 for 语句,但与break一样,它也可以与while循环一起使用。

在这种情况下,continue 用于跳过任何已经生成的值。我们还可以添加一个run_distinct()函数来练习distinct()

def run_distinct():
 items = [5, 7, 7, 6, 5, 5]
 for item in distinct(items):
 print(item)

了解这些发电机!

此时,在继续之前,您应该花一些时间研究这两个生成器。确保您了解它们是如何工作的,以及在它们保持状态时控制是如何进出它们的。如果使用 IDE 运行这些示例,则可以使用调试器跟踪控制流,方法是在生成器和使用它们的代码中放置断点。您可以通过使用 Python 的内置 pdb 调试器(我们稍后将介绍)或者甚至仅仅通过使用老式的打印语句来实现这一点。

不管你怎么做,在进入下一节之前,确保你对这些发电机的工作方式非常熟悉。

惰性生成器管道

现在您已经分别了解了生成器,我们将把它们都安排到一个延迟管道中。我们将一起使用take()distinct()从一个集合中获取前三个独特的项目:

def run_pipeline():
 items = [3, 6, 6, 2, 1, 1]
 for item in take(3, distinct(items)):
 print(item)

请注意,distinct()生成器只做足够的功来满足迭代它的take()生成器的需求

  • 它永远不会到达源列表中的最后两项,因为不需要它们来生成前三个唯一项。这种懒惰的计算方法非常强大,但它产生的复杂控制流可能很难调试。在开发过程中,强制评估所有生成的值通常很有用,通过插入对list()构造函数的调用最容易实现这一点:
take(3, list(distinct(items)))

这个对list()的穿插调用导致distinct()生成器在take()开始工作之前彻底处理其源项。有时,当您调试延迟评估的序列时,这可以为您提供了解发生了什么所需的洞察力。

懒惰与无限

生成器是惰性的,这意味着计算只在请求下一个结果时及时发生。生成器的这一有趣而有用的特性意味着它们可以用来建模无限序列。由于仅根据调用方的请求生成值,并且不需要构建包含序列元素的数据结构,因此可以安全地使用生成器生成永无止境(或非常大)的序列,如:

  • 传感器读数
  • 数学序列(例如:素数、阶乘等)
  • 多 TB 文件的内容

生成 Lucas 级数

请允许我们介绍 Lucas 系列的发电机功能:

def lucas():
 yield 2
 a = 2
 b = 1
 while True:
 yield b
 a, b = b, a + b

卢卡斯级数以21开头,其后的每个值都是前面两个值的总和。因此,序列的前几个值是:

2, 1, 3, 4, 7, 11

第一个收益产生值2。然后,函数初始化ab,这两个值在函数运行时保存所需的“前两个值”。然后该函数进入无限while循环,其中:

  1. 它产生的值为b
  2. ab更新为使用元组解包的整洁应用程序保存新的“前两个”值

现在我们有了一个发电机,它可以像任何其他iterable物体一样使用。例如,要打印 Lucas 编号,可以使用如下循环:

>>> for x in lucas():
...     print(x)
...
2
1
3
4
7
11
18
29
47
76
123
199

当然,由于 Lucas 序列是无限的,它将永远运行,打印值直到计算机内存耗尽。使用Ctrl+C终止循环。

生成器表达式

生成器表达式是理解和生成器函数的交叉。它们使用与理解类似的语法,但它们导致创建一个generator对象,该对象缓慢地生成指定的序列。生成器表达式的语法与列表推导式非常相似:

( expr(item) for item in iterable )

它由括号分隔,而不是用于列表推导式的括号。

生成器表达式对于需要使用声明式简明理解对生成器进行惰性求值的情况非常有用。例如,此生成器表达式生成前一百万个平方数的列表:

>>> million_squares = (x*x for x in range(1, 1000001))

此时,没有创建任何正方形;我们刚刚将序列的规范捕获到一个generator对象中:

>>> million_squares
<generator object <genexpr> at 0x1007a12d0>

我们可以通过使用生成器创建一个(长!)列表来强制评估生成器:

>>> list(million_squares)
. . .
999982000081, 999984000064, 999986000049, 999988000036, 999990000025,
999992000016, 999994000009, 999996000004, 999998000001, 1000000000000]

这个列表显然占用了大量内存——在本例中大约为 40 MB 用于list对象和其中包含的integer对象。

生成器对象只运行一次

请注意,生成器对象只是一个迭代器,一旦以这种方式彻底运行,将不会产生更多的项。重复上一条语句将返回空列表:

>>> list(million_squares)
[]

生成器是一次性使用的对象。每次调用生成器函数时,我们都会创建一个新的generator对象。要从生成器表达式重新创建生成器,必须再次执行表达式本身。

无记忆迭代

让我们通过使用内置的sum()函数计算前千万百万个平方的和来增加赌注,该函数接受一系列可数。如果我们使用列表推导式,我们可以预期这将消耗大约 400 MB 内存。使用时,生成器表达式内存使用将不重要:

>>> sum(x*x for x in range(1, 10000001))
333333383333335000000

这会在一秒钟左右产生一个结果,并且几乎不使用内存。

可选括号

仔细观察,您会发现,在本例中,除了sum()函数调用所需的圆括号外,我们没有为生成器表达式提供单独的圆括号。将圆括号用于函数调用的优雅功能也可用于生成器表达式,从而提高可读性。如果愿意,可以包含第二组括号。

在生成器表达式中使用 if 子句

与理解一样,您可以在生成器表达式的末尾包含一个if-子句。重复使用我们公认低效的is_prime()谓词,我们可以确定前千个素数的整数之和,如下所示:

>>> sum(x for x in range(1001) if is_prime(x))
76127

请注意,这与计算前1000个素数的和不是一回事,这是一个更棘手的问题,因为我们不知道在计算 1000 个素数之前需要测试多少个整数。

电池包括迭代工具

到目前为止,我们已经介绍了 Python 提供的许多创建iterable对象的方法。理解、生成器和遵循 iterable 或迭代器协议的任何对象都可以用于迭代,因此应该清楚,迭代是 Python 的核心功能。

Python 提供了许多用于执行常见迭代器操作的内置函数。这些函数构成了一种用于使用迭代器的词汇表的核心,它们可以组合在一起,以非常简洁、可读的代码生成强大的语句。我们已经遇到了其中的一些函数,包括用于生成整数索引的enumerate()和用于计算数字总和的sum()

介绍 itertools

除了内置函数外,itertools模块还包含大量有用的函数和生成器,用于处理可编辑的数据流。

我们将使用内置的sum()itertools``islice()count()中的两个生成器函数来解决前千个素数问题,从而开始演示这些函数。

早些时候,我们制作了自己的take()生成器函数,用于延迟检索序列的开头。然而,我们不必费心,因为islice()允许我们执行类似于内置列表切片功能的延迟切片。要获得前 1000 个素数,我们需要执行以下操作:

from itertools import islice, count

islice(all_primes, 1000)

但如何生成all_primes?之前,我们一直在使用range()创建原始整数序列,以输入素性测试,但范围必须始终是有限的,即两端都有界。我们想要的是range()的开放式版本,而这正是itertools.count()所提供的。使用count()islice(),我们的前千个素数表达式可以写成:

>>> thousand_primes = islice((x for x in count() if is_prime(x)), 1000)

这将返回一个特殊的islice对象,该对象是 iterable。我们可以使用list构造函数将其转换为列表。

>>> thousand_primes
<itertools.islice object at 0x1006bae10>
>>> list(thousand_primes)
[2, 3, 5, 7, 11, 13 ... ,7877, 7879, 7883, 7901, 7907, 7919]

现在回答我们关于前一千个素数之和的问题很容易,记住重新创建生成器:

>>> sum(islice((x for x in count() if is_prime(x)), 1000))
3682913

布尔序列

另外两个非常有用的内置程序是any()all()。它们与逻辑运算符和或等效,但对于布尔值的可数系列:

>>> any([False, False, True])
True
>>> all([False, False, True])
False

这里我们将使用any()和生成器表达式来回答在13281360范围内是否有素数的问题:

>>> any(is_prime(x) for x in range(1328, 1361))
False

对于完全不同类型的问题,我们可以检查所有这些城市名称是否都是带有大写字母首字母的专有名词:

>>> all(name == name.title() for name in ['London','Paris','Tokyo'])
True

使用 zip 合并序列

我们将要研究的最后一个内置组件是zip(),顾名思义,它为我们提供了一种在两个 iterable 系列上同步迭代的方法。例如,让我们将两列温度数据压缩在一起,一列来自周日,另一列来自周一:

>>> sunday = [12, 14, 15, 15, 17, 21, 22, 22, 23, 22, 20, 18]
>>> monday = [13, 14, 14, 14, 16, 20, 21, 22, 22, 21, 19, 17]
>>> for item in zip(sunday, monday):
...     print(item)
...
(12, 13)
(14, 14)
(15, 14)
(15, 14)
(17, 16)
(21, 20)
(22, 21)
(22, 22)
(23, 22)
(22, 21)
(20, 19)
(18, 17)

我们可以看到,zip()在迭代时生成元组。这反过来意味着我们可以将其与for循环中的元组解包一起使用,以计算这些天每小时的平均温度:

>>> for sun, mon in zip(sunday, monday):
...     print("average =", (sun + mon) / 2)
...
average = 12.5
average = 14.0
average = 14.5
average = 14.5
average = 16.5
average = 20.5
average = 21.5
average = 22.0
average = 22.5
average = 21.5
average = 19.5
average = 17.5

使用 zip()的两个以上序列

事实上,zip()可以接受任意数量的 iterable 参数。让我们添加第三个时间序列,并使用其他内置函数计算相应时间的统计信息:

>>> tuesday = [2, 2, 3, 7, 9, 10, 11, 12, 10, 9, 8, 8]
>>> for temps in zip(sunday, monday, tuesday):
...     print("min = {:4.1f}, max={:4.1f}, average={:4.1f}".format(
...            min(temps), max(temps), sum(temps) / len(temps)))
...
min =  2.0, max=13.0, average= 9.0
min =  2.0, max=14.0, average=10.0
min =  3.0, max=15.0, average=10.7
min =  7.0, max=15.0, average=12.0
min =  9.0, max=17.0, average=14.0
min = 10.0, max=21.0, average=17.0
min = 11.0, max=22.0, average=18.0
min = 12.0, max=22.0, average=18.7
min = 10.0, max=23.0, average=18.3
min =  9.0, max=22.0, average=17.3
min =  8.0, max=20.0, average=15.7
min =  8.0, max=18.0, average=14.3

请注意,我们是如何使用字符串格式化功能将数字列宽控制为四个字符的。

用 chain()延迟连接序列

不过,我们可能希望周日、周一和周二有一个较长的温度系列。我们可以使用itertools.chain()惰性地连接 iterables,而不是通过急切地组合三个温度列表来创建一个新列表:

>>> from itertools import chain
>>> temperatures = chain(sunday, monday, tuesday)

temperatures变量是一个iterable对象,它首先生成sunday中的值,然后生成monday中的值,最后生成tuesday中的值。但是,由于它是惰性的,所以它从不创建包含所有元素的单个列表;事实上,它从不创建任何类型的中间列表!

现在,我们可以检查所有这些温度是否都高于冰点,而不受数据复制的内存影响:

>>> all(t > 0 for t in temperatures)
True

把这一切都集中起来

在总结之前,让我们先从我们一起制作的东西中提取一些片段,让您的计算机计算 Lucas 素数:

>>> for x in (p for p in lucas() if is_prime(p)):
...     print(x)
...
2
3
7
11
29
47
199
521
2207
3571
9349
3010349
54018521
370248451
6643838879
119218851371
5600748293801
688846502588399
32361122672259149

当你已经看够了这些,我们建议你花一些时间探索itertools模块。您越熟悉 Python 对iterables的现有支持,您自己的代码就会变得越优雅和简洁。

总结

  • 理解是描述列表、集合和字典的简明语法。

  • 理解操作在iterable源对象上进行,并应用可选谓词过滤器和强制表达式,这两者通常都是针对当前项的。

  • iterables对象是我们可以逐项迭代的对象。

  • 我们使用内置的iter()函数从iterable对象检索迭代器。

  • 迭代器每次传递到内置的next()函数时,都会从基础iterable系列中逐个生成项。

  • 迭代器在集合耗尽时引发StopIteration异常。

发电机

  • 生成器函数允许我们使用命令式代码描述序列。

  • 生成器函数至少包含一次使用yield关键字。

  • 生成器是迭代器。当迭代器以next()前进时,生成器开始或恢复执行,直到并包括下一个成品。

  • 对生成器函数的每次调用都会创建一个新的generator对象。

  • 生成器可以在迭代之间维护局部变量的显式状态。

  • 生成器是惰性的,因此可以对无限系列的数据进行建模。

  • 生成器表达式具有与列出理解类似的语法形式,并允许以更具声明性和简洁的方式创建generator对象。

迭代工具

Python 包含了一套丰富的工具来处理 iterable 系列,包括内置函数的形式,如sum()any()zip()以及itertools模块。

  1. 我们在本章中详细介绍了iterable协议。

  2. 嗯,他们可以,但是回想一下,在字典上迭代只会产生键!

  3. 我们通常只使用术语生成器来表示生成器函数,尽管有时可能需要区分生成器函数和生成器表达式,我们将在后面介绍。

  4. 作者宣誓在演示或练习中绝不使用斐波那契或快速排序实现。

  5. 这与你看《星球大战》的顺序毫无关系。如果这是您想要的,我们建议您订购 弯刀