2-4-1-迭代器和解析

1. 迭代器

1.1 迭代协议:文件迭代器

已打开的文件对象有个方法名为 readline,可以一次从一个文件中读取一行文本,每次调用 readline 方法时,就会前进到下一列。到达文件末尾时,就会返回空字符串,我们可以通过它来检测,从而跳出循环。

1
f = open('log.txt')
1
f.readline()               # readline 每次调用载入一行
"spam 99 ['eggs']\n"
1
f.readline()
"spam 99 ['eggs']\n"
1
f.readline()
"spam 99 ['eggs']\n"
1
f.readline()               # 文件末尾返回空字符串
''

文件也有一个方法,名为 __next__,差不多有相同的效果:每次调用时,就会返回文件中的下一行。唯一的区别在于,到达文件末尾时,__next__ 会引发内置的 StopIteration 异常,而不是返回空字符串。

1
f = open('log.txt')
1
f.__next__()
"spam 99 ['eggs']\n"
1
f.__next__()
"spam 99 ['eggs']\n"
1
f.__next__()
"spam 99 ['eggs']\n"
1
f.__next__()
---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)

<ipython-input-10-39ec527346a9> in <module>()
----> 1 f.__next__()

StopIteration: 

这个接口就是 Python 中的迭代协议:有 __next__ 方法的对象会前进到下一个结果,而在一系列结果的末尾时,则会引发 StopIteration。

所有迭代工具内部工作起来都是在每次迭代中调用 __next__,并且捕捉 StopIteration 异常来确定何时离开。

对于某些对象,完整的协议包含调用 iter 的额外第一步,但这不是文件所需要的。

读取文本文件的最佳方式就是根本不要去读取,其替代方法就是,让 for 循环在每轮自动调用 __next__ 从而前进到下一行。

1
2
for line in open('log.txt'):
print(line, end='')
spam 99 ['eggs']
spam 99 ['eggs']
spam 99 ['eggs']

这里的 print 使用 end=‘’ 来抑制添加一个 \n,因为行字符串已经有了一个。上例是读取文本文件的最佳方式,原因有三点:

  • 这是最简单的写法
  • 运行最快
  • 从内存使用情况来说也是最好的

1.2 手动迭代:iter 和 next

为了支持手动迭代代码,Python 3.X 还提供了一个内置函数 next,它会自动调用一个对象的 __next__ 方法。给定一个可迭代对象X,调用 next(X) 等同于 X.__next__()

1
2
f = open('log.txt')
next(f)
"spam 99 ['eggs']\n"
1
next(f)
"spam 99 ['eggs']\n"

从技术角度来讲,当 for 循环开始时,会通过迭代协议传给 iter 内置函数,以便从可迭代对象中获得一个迭代器,返回的对象含有需要的 next 方法。iter 函数内部运行 __iter__ 的方法,和 next 与 __next_ 很相似。

完整迭代协议
迭代协议实际上基于两个对象,在迭代工具的两个不同步骤中使用:

  • 请求迭代的可迭代对象,其 __iter__ 由 iter 运行。
  • 在迭代过程中实际生成值的可迭代的返回的迭代器对象,其 __next__ 由 next 运行,并在结束生成结果时引发 StopIteration。

在大多数情况下,迭代工具会自动地安排这些步骤,但是这有助于理解这两个对象的角色。例如,在某些情况下,当只支持一次扫描(例如文件)时,这两个对象是相同的,迭代器对象通常是临时的,由迭代工具在内部使用。

此外,有些对象既是迭代环境工具(它们可以迭代),也是可迭代对象(它们的结果是可迭代的)——包括生成器表达式,以及 Python 3.X 中的 map 和 zip。

在实际代码中,如果我们查看for循环内部如何处理内置的序列类型(如列表),协议的第一步就变得很明显:

1
2
3
L = [1, 2, 3]
I = iter(L) # 从一个可迭代的对象中获得迭代器对象
I.__next__() # 调用迭代器的 next 来获得下一个元素
1
1
I.__next__()
2
1
I.__next__()
3
1
I.__next__()
---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)

<ipython-input-18-ae00d62724fd> in <module>()
----> 1 I.__next__()

StopIteration: 

最初的一步对于文件来说不是必需的,因为文件对象就是自己的迭代器。因为它们只支持一种迭代(它们不能向后查找以支持多个活动扫描),文件有自己的 __next__ 方法,因此不需要像这样返回一个不同的对象:

1
2
f = open('log.txt')
iter(f) is f
True
1
iter(f) is f.__iter__()
True
1
f.__next__()
"spam 99 ['eggs']\n"

列表以及许多其他的内置对象,不是自身的迭代器,因为它们支持多次打开迭代器——例如,在嵌套的循环的不同的位置可以有多个迭代。对这样的对象,我们必须调用 iter 来启动迭代:

1
2
L = [1, 2, 3]
iter(L) is L
False
1
L.__next__()
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-24-77ad3c2310a7> in <module>()
----> 1 L.__next__()

AttributeError: 'list' object has no attribute '__next__'
1
2
I = iter(L)
I.__next__()
1

手动迭代
尽管 Python 迭代工具自动调用这些函数,我们也可以使用它们来手动地应用迭代协议。

1
2
3
L = [1, 2, 3]
for x in L: # 自动迭代
print(x ** 2, end=' ') # 获得 iter,调用 __next__,捕捉异常
1 4 9 
1
2
3
4
5
6
7
I = iter(L)                       # 手动迭代:for 循环经常做的
while True:
try: # try 表达式捕捉异常
X = next(I)
except StopIteration:
break
print(X ** 2, end=' ')
1 4 9 

1.3 其他内置类型迭代器

遍历字典键的经典方法是明确地获取其键的列表。

1
2
3
D = {'a': 1, 'b': 2, 'c': 3}
for key in D.keys():
print(key, D[key])
a 1
b 2
c 3

在最近的 Python 版本中,字典有一个迭代器,在迭代环境中,会自动一次返回一个键。

1
2
I = iter(D)
next(I)
'a'
1
next(I)
'b'
1
next(I)
'c'
1
next(I)
---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)

<ipython-input-32-032af8264890> in <module>()
----> 1 next(I)

StopIteration: 

直接的效果是,不许需要调用 keys 方法来遍历字典键——for 循环将使用迭代协议在每次迭代的时候获取一个键:

1
2
for key in D:
print(key, D[key])
a 1
b 2
c 3

shelves(用于 Python 对象的一个根据键访问的文件系统)和 os.popen(读取 shell 命令的输出的一个工具)的结果也是可迭代的:

1
2
3
import os
P = os.popen('dir')
P.__next__()
' 驱动器 C 中的卷没有标签。\n'
1
next(P)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-37-fd8bddd1ee1f> in <module>()
----> 1 next(P)

TypeError: '_wrap_close' object is not an iterator

在 Python 3.X 中,popen 支持 P.__next__() 方法,但不支持 next(P) 内置函数。由于后者定义来调用前者,这似乎不正常,但是如果用 for 循环或者其他的迭代环境来自动迭代这些对象,都能正确工作。使用顶级 iter 调用也可以。

1
2
I = iter(P)
next(I)
' 卷的序列号是 C8D4-BF33\n'
1
I.__next__()
'\n'

迭代协议也是我们必须把某些结果包装到一个 list 调用中以一次性看到它们的值的原因。可迭代对象一次返回一个结果,而不是一个实际的列表:

1
2
R = range(5)
R
range(0, 5)
1
2
I = iter(R)
next(I)
0
1
next(I)
1
1
list(range(5))
[0, 1, 2, 3, 4]

2. 列表解析

列表解析是最常应用迭代协议的环境之一。在遍历一个列表的时候,可以使用 range 来修改它:

1
2
3
4
L = [1, 2, 3, 4, 5]
for i in range(len(L)):
L[i] += 10
L
[11, 12, 13, 14, 15]

这可能不是 Python 中的最佳实践。我们可以使用列表解析表达式来替代该循环:

1
2
L = [x + 10 for x in L]
L
[21, 22, 23, 24, 25]

列表解析并不完全和 for 循环语句版本相同,因为它产生一个新的列表对象(如果有对最初的列表的多个引用,可能会有关系)。

2.1 列表解析基础知识

从语法上讲,列表解析的语法源自于集合理论表示法中的一个结构,该结构对集合中的每个元素应用一个操作。列表解析看上去就像是一个反向的 for 循环。

列表解析写在一个方括号中,因为它们最终是构建一个新的列表的方式。它们以一个任意的表达式开始,该表达式使用我们所组成的一个循环变量(x + 10)。后面跟着看做是一个 for 循环头部的部分,它声明了循环变量,以及一个可迭代对象(for x in L)。

列表解析并非真的是必需的,我们总是可以用一个 for 循环手动地构建一个表达式结果的列表。但是列表解析编写起来更加精简,并且由于构建结果列表的这种代码样式在 Python 代码中十分常见,因此可以将它们用于多种环境。

此外,列表解析比手动的 for 循环语句运行的更快,因为它们的迭代在解释器内部是以 C 语言的速度执行的。

2.2 在文件上使用列表解析

当我们开始考虑在一个序列中的每项上执行一个操作时,都可以考虑使用列表解析。

1
2
lines = [line.rstrip() for line in open('log.txt')]
lines
["spam 99 ['eggs']", "spam 99 ['eggs']", "spam 99 ['eggs']"]

Python 扫描文件并自动构建了操作结果的一个列表。这是编写这一操作的一种高效的方式。

除了其高效性,列表解析的表现力也很强:

1
[line.upper() for line in open('log.txt')]
["SPAM 99 ['EGGS']\n", "SPAM 99 ['EGGS']\n", "SPAM 99 ['EGGS']\n"]
1
[line.rstrip().upper() for line in open('log.txt')]
["SPAM 99 ['EGGS']", "SPAM 99 ['EGGS']", "SPAM 99 ['EGGS']"]
1
[line.split() for line in open('log.txt')]
[['spam', '99', "['eggs']"],
 ['spam', '99', "['eggs']"],
 ['spam', '99', "['eggs']"]]
1
[line.replace(' ', '!') for line in open('log.txt')]
["spam!99!['eggs']\n", "spam!99!['eggs']\n", "spam!99!['eggs']\n"]

2.3 扩展的列表解析语法

过滤语句:if
表达式中嵌套的 for 循环可以有一个相关的 if 子句,来过滤那些测试不为真的结果项:

1
2
3
L = [1, 2, 3, 4, 5, 6]
ls = [x + 10 for x in L if x % 2 == 0]
ls
[12, 14, 16]

嵌套循环:for
列表解析的完整语法允许任意数目的 for 子句,每个子句有一个可选的相关的 if 子句:

1
[x + y for x in 'abc' for y in 'lmn']
['al', 'am', 'an', 'bl', 'bm', 'bn', 'cl', 'cm', 'cn']

下面是其等价形式,但可能会更慢一些:

1
2
3
4
5
res = []
for x in 'abc':
for y in 'lmn':
res.append(x + y)
res
['al', 'am', 'an', 'bl', 'bm', 'bn', 'cl', 'cm', 'cn']

3. 其他迭代环境

实现了迭代协议的任何工具,都能够在提供了该工具的任何内置类型或用户定义的类上自动地工作。
在对象中从左到右扫描的每种工具都使用了迭代协议,这包括我们介绍过的 for 循环。

列表解析和 map 内置函数都使用了和 for 循环相同的协议。当应用于文件对象时,所有这些使用文件对象的迭代器都自动地按行扫描。

Python 还包含了各种处理迭代的其他内置函数:sorted 排序可迭代对象中的各项,zip 组合可迭代对象中的各项,enumerate 根据相对位置来配对可迭代对象中的项,filter 选择一个函数为真的项,reduce 针对可迭代对象中的成对的项运行一个函数。所有这些都接受一个可迭代的对象,在 Python 3.X 中,zip、enumerate 和 filter 也像 map 一样返回一个可迭代对象。它们实际运行文件的迭代器会自动地按行扫描。

本质上,Python 的内置工具集中从左到右扫描一个对象的每项工具,都定义为在主体对象上使用了迭代协议。甚至包括 list 和 tuple 内置函数,和字符串 join 方法。

甚至一些你可能没有想到的工具也属于这一类。例如,序列赋值、in 成员关系测试、分片赋值和列表的 extend 方法也利用迭代协议进行扫描,从而自动逐行读取文件:

1
2
a, b, c = open('log.txt')
a, b
("spam 99 ['eggs']\n", "spam 99 ['eggs']\n")
1
'spam' in open('log.txt')
False
1
2
3
L = [1, 2, 3, 4]
L[1:3] = open('log.txt')
L
[1, "spam 99 ['eggs']\n", "spam 99 ['eggs']\n", "spam 99 ['eggs']\n", 4]
1
2
3
L = [1]
L.extend(open('log.txt'))
L
[1, "spam 99 ['eggs']\n", "spam 99 ['eggs']\n", "spam 99 ['eggs']\n"]

append 不使用后者(或类似的)在不迭代的情况下向列表中添加可迭代的对象,并可能在以后进行迭代:

1
2
3
L = [1]
L.append(open('log.txt'))
L
[1, <_io.TextIOWrapper name='log.txt' mode='r' encoding='cp936'>]
1
list(L[1])
["spam 99 ['eggs']\n", "spam 99 ['eggs']\n", "spam 99 ['eggs']\n"]

set 调用和集合、字典解析表达式也支持迭代协议。

1
set(open('log.txt'))
{"spam 99 ['eggs']\n"}
1
{line for line in open('log.txt')}
{"spam 99 ['eggs']\n"}

集合和字典解析也支持扩展的列表解析语法,包括 if 测试。

其他的内置函数也支持可迭代协议。sum 调用计算任何可迭代对象中的总数,如果一个可迭代对象中任何的或所有的项为真的时候,any 和 all 内置函数分别返回 True;max 和 min 分别返回一个可迭代对象中最大和最小的项:

1
sum([1, 2, 3, 4, 5])
15
1
any(['spam', '', 'ni'])
True
1
all(['spam', '', 'ni'])
False
1
max([1, 5, 3, 9])
9
1
min([1, 5, 3, 9])
1

严格地讲,max 和 min 函数也可以应用于文件——它们自动使用迭代协议来扫描文件,并且分别选择具有最高的和最低的字符串值的行。

4. Python 3.X 中的新的可迭代对象

4.1 range 迭代器

在 Python 3.X 中,它返回一个迭代器,该迭代器根据需要产生范围中的数字,而不是在内存中构建一个结果列表。

1
2
R = range(10)                    # range 返回一个迭代器,而不是一个列表
R
range(0, 10)
1
2
I = iter(R)                      # 从 range 创造一个迭代器
next(I)
0
1
next(I)
1
1
list(range(10))                   # 强制一个真正的 range 列表
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

range 对象只支持迭代、索引以及 len 函数,不支持任何其他的序列操作。

1
len(R)
10
1
R[0]
0
1
R[-1]
9

4.2 map、zip 和 filter 迭代器

map、zip 和 filter 内置函数在 Python 3.X 中也转变成迭代器以节约内存空间。

和 range 不同,它们都是自己的迭代器——在遍历其结果一次之后,它们就用尽了。换句话说,就是不能在它们的结果上拥有保持不同位置的多个迭代器。

1
2
M = map(abs, (-1, 0, 1))             # map 返回一个迭代器
M
<map at 0x10fe5df84a8>
1
next(M)                               # 手动使用迭代器:用尽结果,不支持 len 或索引
1
1
next(M)
0
1
next(M)
1
1
next(M)
---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)

<ipython-input-13-0666361e9047> in <module>()
----> 1 next(M)

StopIteration: 
1
for x in M: print(x)                     # map 迭代器现在为空:只能遍历一次
1
2
M = map(abs, (-1, 0, 1))                 # 创建一个新的迭代器用于再次扫描
for x in M: print(x) # 迭代环境自动调用 next()
1
0
1

zip 内置函数返回以同样方式工作的迭代器:

1
2
Z = zip((1, 2, 3), (10, 20, 30))
Z
<zip at 0x10fe5df0608>
1
list(Z)
[(1, 10), (2, 20), (3, 30)]
1
for pair in Z: print(Z)                    # 一次遍历后用尽结果

filter 内置函数也是类似的,对于传入的函数返回 True 的可迭代对象中的每一项,它都会返回该项:

1
filter(bool, ['spam', '', 'ni'])
<filter at 0x10fe5e058d0>
1
list(filter(bool, ['spam', '', 'ni']))
['spam', 'ni']

它通常也可以通过扩展列表解析语法来模拟,自动测试真值:

1
[x for x in ['spam', '', 'ni'] if bool(x)]
['spam', 'ni']

4.3 多个迭代器 vs 单个迭代器

range 对象支持 len 和索引,它不是自己的迭代器(手动迭代时,使用 iter 产生一个迭代器),并且,它支持在其结果上的多个迭代器,这些迭代器会记住它们各自的位置:

1
2
R = range(3)                                # range 支持多个迭代器
next(R)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-22-34d331752715> in <module>()
      1 R = range(3)                                # range 支持多个迭代器
----> 2 next(R)

TypeError: 'range' object is not an iterator
1
I1 = iter(R)
1
next(I1)
0
1
next(I1)
1
1
I2 = iter(R)                               # 一个 range 上有两个迭代器
1
next(I2)
0
1
next(I1)                                   # I1 和 I2 的位置不同
2

zip、map 和 filter 不支持相同结果上的多个活跃迭代器:

1
2
3
4
Z = zip((1, 2, 3), (10, 11, 12))
I1 = iter(Z)
I2 = iter(Z) # 一个 zip 上的两个迭代器
next(I1)
(1, 10)
1
next(I1)
(2, 11)
1
next(I2)                                     # I2 和 I1 在同一位置
(3, 12)

使用类来编写自己的可迭代对象的时候,将会看到通常通过针对 iter 调用返回一个新的对象,来支持多个迭代器;单个迭代器一般意味着一个对象返回其自身。生成器函数和表达式的行为就像 map 一样支持单个的活跃迭代器。

4.4 字典视图迭代器

在 Python 3.X 中,字典的 keys、values 和 items 方法返回可迭代的视图对象,它们一次产生一个结果项,而不是在内存中一次产生全部结果列表。

1
2
D = dict(a=1, b=2, c=3)
D
{'a': 1, 'b': 2, 'c': 3}
1
2
K = D.keys()
K
dict_keys(['a', 'b', 'c'])
1
next(K)                                      # 视图自己不是迭代器
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-42-02c2ef8731e9> in <module>()
----> 1 next(K)

TypeError: 'dict_keys' object is not an iterator
1
2
I = iter(K)                                   # 可迭代的视图有一个迭代器
next(I) # 可以手动使用,但不支持 len 和索引
'a'
1
next(I)
'b'
1
for k in D.keys(): print(k, end=' ')
a b c 

和所有的迭代器一样,我们总可以通过把一个 Python 3.X 字典视图传递到 list 内置函数中,从而强制构建一个真正的列表:

1
2
K = D.keys()
list(K)
['a', 'b', 'c']
1
K[0]
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-48-abefddd6f6bf> in <module>()
----> 1 K[0]

TypeError: 'dict_keys' object does not support indexing
1
list(D.items())
[('a', 1), ('b', 2), ('c', 3)]

Python 3.X 字典仍然有自己的迭代器,它返回连续的键,因此,无需直接在此环境中调用 keys:

1
2
I = iter(D)
next(I)
'a'
1
next(I)
'b'
1
for key in D: print(key, end=' ')             # 不必调用 keys 来进行迭代,但 keys 在 3.X 中也是一个迭代器
a b c 

由于 keys 不再返回一个列表,对键进行排序来扫描一个字典可以使用 list 来转换 keys 视图,或者在一个键视图或字典自身上使用 sorted 调用:

1
for k in sorted(D.keys()): print(k, D[k], end=' ')
a 1 b 2 c 3 
1
for k in sorted(D): print(k, D[k], end=' ')
a 1 b 2 c 3