Python 中的 Iterator

Iterators

迭代器(iterator)是 Python 的一个语言特性,它是编写函数式风格程序的重要基础。

迭代器是表示数据流的对象;这个对象一次返回一个元素的数据。Python 迭代器必须提供 __next__() 方法,该方法不带参数并且总是返回数据流中的下一个元素。如果数据流中没有更多元素了,__next__() 必须抛出 StopIteration 异常。迭代器不必是有限的,编写一个产生无限数据流的迭代器是完全合理的。

内置函数 iter() 接受一个任意对象并尝试返回一个会返回该对象的内容或者元素的迭代器,如果该对象是不可迭代的则抛出 TypeError 异常。Python 有数个内建数据类型支持迭代,最常见的是列表和字典。如果你可以从一个对象获得迭代器,那么我们称这个对象是可迭代的。

你可以手动体验迭代器接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> L = [1, 2, 3]
>>> it = iter(L)
>>> it
<...iterator object at ...>
>>> it.__next__() # same as next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>

Python 期望在几个不同的环境中使用可迭代对象,其中最重要的就是 for 语句。在语句 for X in Y 中,Y 必须是迭代器或者其他可通过 iter() 创建迭代器的对象。以下两个语句是等价的:

1
2
3
4
5
for i in iter(obj):
print(i)

for i in obj:
print(i)

可是使用 list() 或 tuple() 构造函数将迭代器实现为列表或元组:

1
2
3
4
5
>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> t = tuple(iterator)
>>> t
(1, 2, 3)

序列拆包也支持迭代器:如果你知道一个迭代器会返回 N 个元素,你可以把它们拆包到一个 N 元组中:

1
2
3
4
5
>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> a, b, c = iterator
>>> a, b, c
(1, 2, 3)

内置函数如 max() 和 min() 接受单个迭代器对象并返回最大或最小元素。运算符 innot in 也支持迭代器:如果 X 在迭代器返回到流中存在则 X in interator 为真。如果迭代器是无限的,你会会遇到明显的问题:max(),min() 永远不会结束,同样如果元素 X 不在流中,运算符 innot in 也不会结束。

要注意的是,在迭代器中你只能往前;没有办法获取之前的元素,重置迭代器,或者复制它。迭代器对象可以选择提供这些附加功能,但迭代器协议仅指定了 __next__() 方法。因此函数可能会耗尽迭代器的所有输出,如果你需要在流中进行不同的操作,只能创建一个新的迭代器。

Data Types That Support Iterators

我们已经看到了列表和元组如何支持迭代器。实际上,任何 Python 序列类型(如字符串)都自动支持迭代器的创建。

对字典调用 iter() 会返回一个遍历字典的键的迭代器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> m = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
... 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
>>> for key in m:
... print(key, m[key])
Jan 1
Feb 2
Mar 3
Apr 4
May 5
Jun 6
Jul 7
Aug 8
Sep 9
Oct 10
Nov 11
Dec 12

注意,从 Python 3.7 开始,字典迭代顺序和插入顺序相同。在更早的版本,该行为未指定,实现可能不同。

对字典应用 iter() 总是遍历它的键,但字典有返回其他迭代器的方法。如果你想要迭代它的值或者键值对,可以显示调用 values() 或 items() 方法获得适合的迭代器。

字典构造器 dict() 接受返回有穷 (key, value) 元组流的迭代器:

1
2
3
>>> L = [('Italy', 'Rome'), ('France', 'Paris'), ('US', 'Washington DC')]
>>> dict(iter(L))
{'Italy': 'Rome', 'France': 'Paris', 'US': 'Washington DC'}

通过调用 readline() 方法直到文件中没有更多的行,文件也支持迭代。这意味着你可以像这样读取文件中的每一行:

1
2
3
for line in file:
# do something for each line
...

集合可以从迭代中获取其内容,并让您迭代集合的元素:

1
2
3
S = {2, 3, 5, 7, 11, 13}
for i in S:
print(i)

Generator expressions and list comprehensions

对迭代器输出的两个常规操作是 1)对每个元素进行一些操作,2)选择符合某种条件的元素子集。例如,给定一个字符串列表,你可能想要去除每一行结尾的空格或提取包含给定子字符串的所有字符串。

列表推导(list comprehensions)生成器表达式(generator expressions)(缩写:“listcomps” 和 “genexps”)是这种操作的简明表示法,借鉴了函数式编程语言 Haskell。你可以使用以下代码从字符串流中删除所有的空格:

1
2
3
4
5
6
7
line_list = ['  line 1\n', 'line 2  \n', ...]

# Generator expression -- returns iterator
stripped_iter = (line.strip() for line in line_list)

# List comprehension -- returns list
stripped_list = [line.strip() for line in line_list]

通过添加 “ if ” 条件来选择某些元素:

1
2
stripped_list = [line.strip() for line in line_list
if line != ""]

使用列表推导,你得到一样 Python 列表;stripped_list 是包含结果行的列表,而不是迭代器。生成器表达式返回一个迭代器,它根据需要计算值,而不需要一次实现所有的值。这意味着,如果你使用返回无限流或者大量数据的迭代器时,列表推导是无效的。生成器表达式更适合这些情况。

生产器表达式使用圆括号(”()”),列表推导使用方括号(”[]”)。生成器表达式的形式如下:

1
2
3
4
5
6
7
8
( expression for expr in sequence1
if condition1
for expr2 in sequence2
if condition2
for expr3 in sequence3 ...
if condition3
for exprN in sequenceN
if conditionN )

同样,对于列表推导,只有外部括号不同(方括号而不是圆括号)。

生成的输出元素就是 expressions 的连续的值。if 子句都是可选的;如果存在,则 expression 只在 condition 为真时执行并添加到结果中。

生成器表达式总是必须写在圆括号内,即使是发起函数调用的圆括号也可以。如果要创建一个立即传给一个函数的迭代器,你可以这样写:

1
obj_total = sum(obj.count for obj in list_all_objects())

for...in 子句包含被迭代的序列。这些序列不需要长度相同,因为它们是被从左到右迭代,而不是并行的。对 sequence1,sequence2 中的元素都是从头开始遍历。接着 sequence3 遍历 sequence1 和 sequence2 的每个结果对元素。

换句话说,列表推导和生成器表达式等价于以下 Python 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
for expr1 in sequence1:
if not (condition1):
continue # Skip this element
for expr2 in sequence2:
if not (condition2):
continue # Skip this element
...
for exprN in sequenceN:
if not (conditionN):
continue # Skip this element

# Output the value of
# the expression.

这意味着,但有多重的 for...in 子句而没有 if 条件时,结果输出的长度等于所有序列的长度的乘积。如果有两个长度为 3 的列表,输出列表长度为 9 个元素。

1
2
3
4
5
6
>>> seq1 = 'abc'
>>> seq2 = (1, 2, 3)
>>> [(x, y) for x in seq1 for y in seq2]
[('a', 1), ('a', 2), ('a', 3),
('b', 1), ('b', 2), ('b', 3),
('c', 1), ('c', 2), ('c', 3)]

为了避免在 Python 语法中引入歧义,如果 expression 要创建一个元组,它必须用圆括号括起来。下面第一个列表推导时错误的,而第二种是正确的:

1
2
3
4
# Syntax error
[x, y for x in seq1 for y in seq2]
# Correct
[(x, y) for x in seq1 for y in seq2]