在本系列的第1部分中,您研究了Python迭代器以及创建自己的迭代器的最直接的方法,即通过编写生成器函数或表达式。 一旦掌握了基础知识,您将很快开始拥有用于构建迭代器的各种想法。 但是,确保您没有重新发明轮子是一个好主意。
Python的标准库是有价值的支持和工具的奇迹,其中一些比其他的更深层次。 在操纵迭代器时,有几个有用的模块,但是首先,您应该查看内置函数中的一些可用功能,这意味着在大多数情况下,您无需导入任何内容即可使用它们。
使用map
函数,您可以在迭代器中处理每个项目,从而为您提供一个新的结果迭代器。
>>> r = range(10)
>>> m = map(str, r)
>>> next(m)
'0'
>>> list(m)
['1', '2', '3', '4', '5', '6', '7', '8', '9']
map的第一个参数是映射函数,该函数适用于每个项目。 在这种情况下,我使用str
将范围内的每个整数转换为字符串。 当我从地图迭代器请求第一项时,我得到一个字符串。 然后,我使用list提取序列中的所有内容。 请注意,列表以1
而不是0
开头。 那是因为我已经从地图中提取了第一项。 与列表或元组不同,迭代器不支持随机访问。 您一次向前获得一个物品。
当然,您可以编写自己的映射函数。 它期望从map
一个输入值。 将以下内容粘贴到您的解释器会话中:
def get_letter(i):
return chr(ord('a')+i)
在这里,我使用ord
来获得与字符'a'
相对应的数字代码,并添加函数的参数以得出新的字符代码。 函数chr
将此代码转换回字符串字符。 让我们将此功能与map
。
>>> r = range(10)
>>> list(map(get_letter, r))
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
此时,您可能还记得第1部分,并且意识到这很像生成器表达式。 确实,我本可以做到以下几点。
>>> list( ( get_letter(i) for i in range(10) ) )
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
请注意,我是如何将生成器表达式直接作为list
的参数放入的。 这应该有助于说明您可以像其他Python表达式一样使用生成器表达式。 在这种情况下, get_letter
函数非常简单,您甚至可以将其直接构建到生成器表达式中。
>>> list( ( chr(ord('a')+i) for i in range(10) ) )
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
这似乎是多余的,但是map在Python中存在的时间超过了生成器表达式,并且在某些特殊情况下它仍然很方便。 不过,在大多数情况下,您可能会使用生成器表达式进行迭代器的这种简单操作。
有时,您不想将一个迭代器变成另一个迭代器,而是从一个迭代器计算一个单个值。
>>> sum(range(20, 30))
245
sum
函数采用数字迭代器(认为是int或float),并返回迭代器中所有项目的总和,在这种情况下,所有数字的总和为20到30。这称为归约,其中一系列值是减少到一个新值。 Python还提供了通用的归约函数。 首先粘贴以下功能:
def sum_of_squares(acc, i):
return acc + i**2
使用它减少如下。 您需要functools
模块。
>>> import functools
>>> functools.reduce(sum_of_squares, range(10))
285
第一个参数是一个带有两个参数的函数。 它需要一个累加值,然后需要一个新值以某种方式组合成一个更新的累加值,然后将其返回。 在这种情况下,累计值是到目前为止的项目平方和。
就像生成器表达式比使用map
更具可读性一样,通常有更多可读性的替代方法可以减少,但是对于某些特定用途(对map
来说是概念上的对点)有用。
map
的想法是采用迭代器并生成派生的迭代器。 reduce
的想法是采用一个迭代器并返回一个从其所有项目派生的值。 事实证明,这种概念组合是考虑处理大量数据的有效方法。
我想提到一种特殊的方法,它可以减少费用。
>>> tenletters = ( chr(ord('a')+i) for i in range(10) )
>>> '!'.join(tenletters)
'a!b!c!d!e!f!g!h!i!j'
字符串的join
方法采用一个迭代器,并通过将迭代器项字符串与指定的字符串连接在一起来创建新的字符串。 玩多一点:
>>> '!'.join(tenletters)
''
>>> tenletters = ( chr(ord('a')+i) for i in range(10) )
>>> ''.join(tenletters)
'abcdefghij'
>>> tenletters = ( chr(ord('a')+i) for i in range(10) )
>>> ' and '.join(tenletters)
'a and b and c and d and e and f and g and h and i and j'
第一行的输出很好地提醒了迭代器是一种方法,一旦用完,您将无法再使用它们。 请注意,因为这是使用迭代器的代码错误的常见原因。 在下一行中,我设置了一个新的生成器,您可以看到在空字符串上的联接只是将给定迭代器中的所有字符串连接在一起。 您也可以使用更长的字符串,如最后两行所示。
您还可以使用另一个内置函数来创建仅包含输入一部分的迭代器。 在第1部分中 ,我介绍了以下示例。
>>> import math
>>> notby2or3 = ( n for n in range(1, 20) if math.gcd(n, 2) == 1 and math.gcd(n, 3) == 1 )
>>> list(notby2or3)
[1, 5, 7, 11, 13, 17, 19]
返回并检查第1部分 ,是否需要对notby2or3
函数进行复习。 您可以使用filter函数来实现与该生成器表达式等效的函数(再次),要意识到这一点。
>>> import math
>>> def notby2or3(n):
... return math.gcd(n, 2) == 1 and math.gcd(n, 3) == 1
...
>>> list(filter(notby2or3, range(1, 20)))
[1, 5, 7, 11, 13, 17, 19]
使用所谓的lambda
函数,我本来可以少写这两行,但这超出了本教程系列的范围。
有很多小的迭代器使用模式。 itertools
模块提供了其中许多功能的高性能实现,值得学习。
假设您想创建一个更长的迭代器,该迭代器将一系列其他迭代器串联起来:
>>> import itertools
>>> it = itertools.chain(range(5), range(5, 0, -1), range(5))
>>> list(it)
[0, 1, 2, 3, 4, 5, 4, 3, 2, 1, 0, 1, 2, 3, 4]
将itertools.chain
传递给迭代器作为函数参数,它会生成这些输出的链。 如果有一个迭代器的列表或元组序列,则可以对可变位置函数参数使用Python的特殊语法。
>>> list_of_iters = [range(5), range(5, 0, -1), range(5)]
>>> it = itertools.chain(*list_of_iters)
>>> list(it)
[0, 1, 2, 3, 4, 5, 4, 3, 2, 1, 0, 1, 2, 3, 4]
您可以对功能变量参数使用任何迭代器。 它不必是列表或元组。 粘贴以下内容:
def forth_back_forth(n):
yield range(n)
yield range(n, 0, -1)
yield range(n)
您可以使用此生成器向itertools.chain提供其参数:
>>> it = itertools.chain(*forth_back_forth(3))
>>> list(it)
[0, 1, 2, 3, 2, 1, 0, 1, 2]
forth_back_forth
和itertools.chain
的组合使我想知道进一步扩大后退/前进/后退数字模式的感觉。 粘贴以下内容:
def zigzag_iters(period):
while True:
yield range(period)
yield range(period, 0, -1)
然后,尝试一下:
>>> it = zigzag_iters(2)
>>> next(it)
range(0, 2)
>>> next(it)
range(2, 0, -1)
>>> next(it)
range(0, 2)
>>> next(it)
range(2, 0, -1)
>>> next(it)
range(0, 2)
如果您继续输入next(it)
,它将继续在两个输出范围之间反复循环,因为while True
实施了无限循环,并且其中没有代码可以中断循环的执行。 该生成器处于暂停和恢复的恒定状态,直到Python进程(在这种情况下为交互式解释器)终止。 或者,您可以在任何生成器对象上使用close()
方法结束它,而不管它在何处执行。
您可能已经注意到,我没有使用list
来说明迭代器的内容。 如果尝试执行此操作,则基本上是在尝试创建一个无限列表,当然,您将耗尽内存。
还请记住,这是返回迭代器,而不是项目。 使用forth_back_forth
的想法是使用itertools.chain
从那些迭代器中获取项目。 您不能使用zigzag_iters
进行相同的操作,因为这还会尝试创建无限项的集合,并且函数执行前必须知道函数的所有参数,即使在变量参数的情况下也是如此。 这将意味着Python运行时将尝试从无限迭代器获取所有项目,而这是不可能的。
一旦开始使用生成器,无限迭代器实际上是一个有用的概念。 但是,您必须知道尝试从此类迭代器的内容构建任何集合的操作。
因为您不能将itertools.chain
与zigzag_iters
一起使用, zigzag_iters
创建一个更直接的版本。
def zigzag(period):
while True:
for n in range(period):
yield n
for n in range(period, 0, -1):
yield n
将其粘贴并查看迭代器如何无限期地继续:
>>> it = zigzag(2)
>>> next(it)
0
>>> next(it)
1
>>> next(it)
2
>>> next(it)
1
>>> next(it)
0
>>> next(it)
1
>>> next(it)
2
>>> next(it)
1
>>> next(it)
2
>>> next(it)
1
>>> next(it)
0
>>> next(it)
1
这是另一种常见的模式,其中外部迭代器是一个或多个内部迭代器的级联。 众所周知,Python为此提供了一种标准语法,即yield from
语句的yield from
,从以下功能上相同的zigzag
重写中可以看出。
def zigzag(period):
while True:
yield from range(period)
yield from range(period, 0, -1)
如果您想到一个迭代器来提供另一个迭代器的全部,请考虑yield from
。 在这种情况下,提供的迭代器将被完全消耗。 如果它是无限迭代器,则外部迭代器也将是无限迭代器,并无限期地继续执行语句的yield from
。
为了完整zigzag_iters
,最后一点要注意:前面,我说过不能将zigzag_iters
函数与itertools.chain
一起使用,因为它会尝试创建函数参数的无限集合。 itertools
背后的思想家也想到了这一点,并提供了一个可行的变体itertools.chain.from_iterable
。
>>> it = itertools.chain.from_iterable(zigzag_iters(2))
>>> next(it)
0
>>> next(it)
1
>>> next(it)
2
>>> next(it)
1
>>> next(it)
0
>>> next(it)
1
>>> next(it)
2
>>> next(it)
1
>>> next(it)
0
>>> next(it)
1
>>> next(it)
2
>>> next(it)
1
您会在itertools
找到一些方便的例程来处理无限迭代器。 一个是itertools.islice
。 这样,您可以从迭代器(包括无限迭代器)中提取子集序列。
>>> it1 = zigzag(5)
>>> it2 = itertools.islice(it1, 0, 20)
>>> list(it2)
[0, 1, 2, 3, 4, 5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5, 4, 3, 2, 1]
>>> it2 = itertools.islice(it1, 1, 20)
>>> list(it2)
[1, 2, 3, 4, 5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5, 4, 3, 2, 1]
>>> it2 = itertools.islice(it1, 0, 20, 2)
>>> list(it2)
[0, 2, 4, 4, 2, 0, 2, 4, 4, 2]
该函数从迭代器的开始索引和结束索引中摘录。 您还可以选择提供一个步骤,因此在某些方面它感觉类似于range
。 但是请注意:与range
不同,不允许使用负步长值。
itertools
最有用的功能之一也是它最itertools
功能之一,但值得一读。
使用itertools.groupby
根据某些条件对迭代器进行分段。 这是将小时划分为十二小时周期的象限的一种方法。
>>> hours = range(12)
>>> def divide_by_3(n): return n//3
...
>>> it = itertools.groupby(hours, divide_by_3)
>>> next(it)
(0, )
您为itertools.groupby
提供了一个输入迭代器和一个分组函数,该函数在该迭代器的每个项目上执行。 当分组功能的结果从一个项目更改为下一个项目时,将创建一个新的组,其中包含所有项目,直到分组功能值的下一个更改为止。 分组函数divide_by_3
使用地板除法运算符//进行除法,然后舍入到下一个最小整数。
图1在此示例中有助于可视化itertools.groupby
的操作。
理解itertools.groupby
的关键是要记住分组功能值的变化是导致输出中分组项目的原因。 通过一些实践,您将能够熟练地为各种用例编写分组功能。
让我们进一步研究一下itertools.groupby
示例。
>>> hours = range(12)
>>> for quadrant, group_it in itertools.groupby(hours, divide_by_3):
... print('Items in quadrant', quadrant)
... for i in group_it:
... print(i)
...
Items in quadrant 0
0
1
2
Items in quadrant 1
3
4
5
Items in quadrant 2
6
7
8
Items in quadrant 3
9
10
11
在这里,外部循环调用itertools.groupby
,但是循环中的每个项目都是一个元组。 如图1所示 ,该元组的第二个元素group_it
依次是一个迭代器。 因此,内部循环适用于每个组的迭代器。
为了结合本教程中的几个概念,可以将分组与无限迭代器一起使用。 首先查看itertools中的功能,它巧妙地创建了无限的迭代器。
>>> inf_it = itertools.cycle(range(12))
>>> print(list(itertools.islice(inf_it, 32)))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 6, 7]
在第一行中,我使用itertools.cycle
,它在您提供的有限迭代器上创建了无限循环。 在这种情况下,它只会一遍又一遍地产生0到11的序列。 我使用itertools.islice
安全地获取它的一个子集并将其打印为列表。 还有itertools.repeat
,它采用一个值并反复迭代一个值。
将itertools.groupby
和itertools.cycle
组合是本教程中要研究的最后一个工具。
>>> inf_it = itertools.cycle(range(12))
>>> hours_32 = itertools.islice(inf_it, 32)
>>> for quadrant, group_it in itertools.groupby(hours_32, divide_by_3):
... print('Items in quadrant', quadrant, ':', list(group_it))
...
Items in quadrant 0 : [0, 1, 2]
Items in quadrant 1 : [3, 4, 5]
Items in quadrant 2 : [6, 7, 8]
Items in quadrant 3 : [9, 10, 11]
Items in quadrant 0 : [0, 1, 2]
Items in quadrant 1 : [3, 4, 5]
Items in quadrant 2 : [6, 7, 8]
Items in quadrant 3 : [9, 10, 11]
Items in quadrant 0 : [0, 1, 2]
Items in quadrant 1 : [3, 4, 5]
Items in quadrant 2 : [6, 7]
仔细研究本课程,以了解这些迭代器工具如何相互作用。 例如,您可以看到小时数的循环如何导致分组功能的循环。
您开始看到可以多么紧凑地将基本构建块组合在一起以产生有趣的迭代器模式。 当您将这些功能与自己的专用生成器结合使用时,这将成为解决有效处理串行数据问题的强大范例。
itertools的功能超出了本教程中的能力范围,但是您现在已经对它的一些产品有了基本的了解,并且可以很好地了解它的最棘手但也可能是最有价值的groupby
。
与整个标准库一样,您可以找到该模块的全面文档 ,但是像往常一样,这是一个精通开发人员检查细节的更多内容。 它不是作为入门手册设计的。 我建议您从本教程和上一教程的构建块开始,并逐一尝试使用itertools的功能,内置的可用其他功能以及用于迭代器的标准库。
在本系列的第3部分中,从更荒诞的角度出发。 您将探索协程,一种特殊的生成函数。 您还将学习另一个强大但棘手的标准库模块: asyncio
。
翻译自: https://www.ibm.com/developerworks/library/ba-on-demand-data-python-2/index.html