迭代器
9.1 迭代器和 iter() 函数
9.1.1 什么是迭代器?
它为类序列对象提供了一个类序列的接口,你可以迭代不是序列但表现出序列行为的对象
9.1.2 为什么要迭代器?
援引 PEP (234) 中对迭代器的定义:
提供了可扩展的迭代器接口.
对列表迭代带来了性能上的增强.
在字典迭代中性能提升.
创建真正的迭代接口, 而不是原来的随机对象访问.
与所有已经存在的用户定义的类以及扩展的模拟序列和映射的对象向后兼容
迭代非序列集合(例如映射和文件)时, 可以创建更简洁可读的代码.
9.1.3 如何迭代?
根本上说, 迭代器就是有一个 next() 方法的对象, 而不是通过索引来计数. 当你或是一个循环机制(例如 for 语句)需要下一个项时, 调用迭代器的 next() 方法就可以获得它. 条目全部取出后, 会引发一个 StopIteration 异常, 这并不表示错误发生, 只是告诉外部调用者, 迭代完成.
不过, 迭代器也有一些限制. 例如你不能向后移动, 不能回到开始, 也不能复制一个迭代器.如果你要再次(或者是同时)迭代同个对象, 你只能去创建另一个迭代器对象. 不过, 这并不糟糕,因为还有其他的工具来帮助你使用迭代器.
reversed() 内建函数将返回一个反序访问的迭代器.
enumerate() 内建函数同样也返回迭代器.
any() 和 all() , 如果迭代器中某个/所有条目的值都为布尔真时,则它们返回值为真.
上一章,我们展示了如何在 for 循环中通过索引或是可迭代对象来遍历条目. 同时 Python 还提供了一整个 itertools 模块, 它包含各种有用的迭代器.
9.1.4 使用迭代器
9.1.4.1 序列
正如先前提到的, 迭代 Python 的序列对象和你想像的一样:
>>> myTuple = (123, 'xyz', 45.67)
>>> i = iter(myTuple)
>>> i.next()
123
>>> i.next()
'xyz'
>>> i.next()
45.67
>>> i.next()
Traceback (most recent call last):
File "", line 1, in ?
StopIteration
如果这是一个实际应用程序, 那么我们需要把代码放在一个 try-except 块中. 序列现在会自动地产生它们自己的迭代器, 所以一个 for 循环:
for i in seq:
do_something_to(i)
实际上是这样工作的:
fetch = iter(seq)
while True:
try:
i = fetch.next()
except StopIteration:
break
do_something_to(i)
不过, 你不需要改动你的代码, 因为 for 循环会自动调用迭代器的 next() 方法(以及监视StopIteration 异常)
9.1.4.2 字典
字典和文件是另外两个可迭代的 Python 数据类型. 字典的迭代器会遍历它的键(keys).语句 for eachKey in myDict.keys() 可以缩写为 for eachKey in myDict , 例如:
>>> legends = { ('Poe', 'author'): (1809, 1849, 1976),
... ('Gaudi', 'architect'): (1852, 1906, 1987),
... ('Freud', 'psychoanalyst'): (1856, 1939, 1990)
... }
...
>>> for eachLegend in legends:
...print 'Name: %s\tOccupation: %s' % eachLegend
...print ' Birth: %s\tDeath: %s\tAlbum: %s\n' \
...% legends[eachLegend]
Name: Freud Occupation: psychoanalyst
Birth: 1856 Death: 1939 Album: 1990
Name: Poe Occupation: author
Birth: 1809 Death: 1849 Album: 1976
Name: Gaudi Occupation: architect
Birth: 1852 Death: 1906 Album: 1987
9.1.4.3 文件
文件对象生成的迭代器会自动调用 readline() 方法. 这样, 循环就可以访问文本文件的所有行 .程序员可以使用更简单的for eachLine in myFile替换for eachLine in myFile.readlines() :
>>> myFile = open('config-win.txt')
>>> for eachLine in myFile:
... print eachLine, # comma suppresses extra \n
...
[EditorWindow]
font-name: courier new
font-size: 10
>>> myFile.close()
9.1.5 可变对象和迭代器
记住,在迭代可变对象的时候修改它们并不是个好主意. 这在迭代器出现之前就是一个问题.一个流行的例子就是循环列表的时候删除满足(或不满足)特定条件的项:
for eachURL in allURLs:
if not eachURL.startswith('http://'):
allURLs.remove(eachURL)
# YIKES!!
除列表外的其他序列都是不可变的, 所以危险就发生在这里. 一个序列的迭代器只是记录你当前到达第多少个元素, 所以如果你在迭代时改变了元素, 更新会立即反映到你所迭代的条目上.在迭代字典的 key 时, 你绝对不能改变这个字典. 使用字典的 keys() 方法是可以的, 因为keys() 返回一个独立于字典的列表. 而迭代器是与实际对象绑定在一起的, 它将不会继续执行下去:
>>> myDict = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
>>> for eachKey in myDict:
... print eachKey, myDict[eachKey]
... del myDict[eachKey]
...
a 1
Traceback (most recent call last):
File "", line 1, in ?
RuntimeError: dictionary changed size during iteration
这样可以避免有缺陷的代码.
9.1.6 如何创建迭代器
对一个对象调用 iter() 就可以得到它的迭代器. 它的语法如下:
iter(obj) #把可迭代对象转为迭代器
iter(func, sentinel )#第一个参数,任何可调用对象,可以是函数。第二个是标记值,当可调用对象返回这个值,迭代器抛出StopIteration异常,而不是产生标记值
>>> from random import choice
>>> values=[x for x in range(8)]
>>> def test_iter():
... return choice(values)
...
>>> it=iter(test_iter,2)
>>> it
<callable-iterator object at 0x7f8767307a90>
>>> for i in it:
... print i
...
5
6
3
4
9.1.7 列表解析
9.1.7.1 列表解析的语法:
[expr for iter_var in iterable]
列表解析还提供了一个扩展版本的语法:
[expr for iter_var in iterable if cond_expr]
9.1.7.2 矩阵样例
你需要迭代一个有三行五列的矩阵么? 很简单:
>>> [(x+1,y+1) for x in range(3) for y in range(5)]
[(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (2, 2), (2,
3), (2, 4), (2, 5), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5)]
9.1.7.3 磁盘文件样例
假设我们有如下这样一个数据文件 hhga.txt , 需要计算出所有非空白字符的数目:
And the Lord spake, saying, “First shalt thou take
out the Holy Pin. Then shalt thou count to three,
no more, no less. Three shall be the number thou shalt
count, and the number of the counting shall be three.
Four shalt thou not count, nei- ther count thou two,
excepting that thou then proceed to three. Five is
right out. Once the number three, being the third
number, be reached, then lobbest thou thy Holy Hand
Grenade of Antioch towards thy foe, who, being
naughty in My sight, shall snuff it.”
我们已经知道可以通过 for line in data 迭代文件内容, 不过, 除了这个, 我们还可以把每行分割( split )为单词, 然后我们可以像这样计算单词个数:
>>> f = open('hhga.txt', 'r')
>>> len([word for line in f for word in line.split()])
91
快速地计算文件大小
import os
>>> os.stat('hhga.txt').st_size
假定文件中至少有一个空白字符, 我们知道文件中有少于 499 个非空字符. 我们可以把每个单词的长度加起来, 得到和.
>>> f.seek(0)
>>> sum([len(word) for line in f for word in line.split()])
408
这里我们用 seek() 函数回到文件的开头, 因为迭代器已经访问完了文件的所有行. 一个清晰明了的列表解析完成了之前需要许多行代码才能完成的工作! 如你所见, 列表解析支持多重嵌套for 循环以及多个 if 子句. 完整的语法可以在官方文档中找到. 你也可以在 PEP 202 中找到更多关于列表解析的资料
9.1.8 生成器表达式
9.1.8.1 生成器表达式是列表解析的一个扩展
列表解析的一个不足就是必要生成所有的数据, 用以创建整个列表. 这可能对有大量数据的迭代器有负面效应.
生成器表达式通过结合列表解析和生成器解决了这个问题.它与列表解析非常相似,而且它们的基本语法基本相同;不过它并不真正创建数字列表, 而是返回一个生成器,这个生成器在每次计算出一个条目后,把这个条目“产生”(yield)出来. 生成器表达式使用了"延迟计算"(lazy evaluation), 所以它在使用内存上更有效. 我们来看看它和列表解析到底有多相似:
列表解析:
[expr for iter_var in iterable if cond_expr]
生成器表达式:
(expr for iter_var in iterable if cond_expr)
生成器并不会让列表解析废弃, 它只是一个内存使用更友好的结构, 基于此, 有很多使用生成器地方. 下面我们提供了一些使用生成器表达式的例子, 最后例举一个冗长的样例, 从它你可以感觉到 Python 代码在这些年来的变化.
9.1.8.2 磁盘文件样例(2)
在前边列表解析一节, 我们计算文本文件中非空白字符总和. 最后的代码中, 我们展示了如何使用一行列表解析代码做所有的事. 如果这个文件的大小变得很大, 那么这行代码的内存性能会很低, 因为我们要创建一个很长的列表用于存放单词的长度.
为了避免创建庞大的列表, 我们可以使用生成器表达式来完成求和操作. 它会计算每个单词的长度然后传递给 sum() 函数(它的参数不仅可以是列表,还可以是可迭代对象,比如生成器表达式).这样, 我们可以得到优化后的代码(代码长度, 还有执行效率都很高效):
>>> sum(len(word) for line in data for word in line.split())
408
我们所做的只是把方括号删除: 少了两字节, 而且更节省内存 … 非常地环保!
9.1.8.3 交叉配对例子
生成器表达式就好像是懒惰的列表解析(这反而成了它主要的优势). 它还可以用来处理其他列表或生成器, 例如这里的 rows 和 cols :
rows = [1, 2, 3, 17]
def cols(): # example of simple generator
yield 56
yield 2
yield 1
不需要创建新的列表, 直接就可以创建配对. 我们可以使用下面的生成器表达式:
x_product_pairs = ((i, j) for i in rows for j in cols())
现在我们可以循环 x_product_pairs , 它会懒惰地循环 rows 和 cols :
>>> for pair in x_product_pairs:
... print pair
...
(1, 56)
(1, 2)
(1, 1)
(2, 56)
(2, 2)
(2, 1)
(3, 56)
(3, 2)
(3, 1)
(17, 56)
(17, 2)
(17, 1)
===
9.1.8.4 重构样例(重点)
我们通过一个寻找文件最长的行的例子来看看如何改进代码. 在以前, 我们这样读取文件:
f = open('/etc/motd', 'r')
longest = 0
while True:
linelen = len(f.readline().strip())
if not linelen:
break
if linelen > longest:
longest = linelen
f.close()
return longest
事实上, 这还不够老. 真正的旧版本 Python 代码中, 布尔常量应该写是整数 1 , 而且我们应该使用 string 模块而不是字符串的 strip() 方法:
import string
:
len(string.strip(f.readline()))
从那时起, 我们认识到如果读取了所有的行, 那么应该尽早释放文件资源. 如果这是一个很多进程都要用到的日志文件, 那么理所当然我们不能一直拿着它的句柄不释放. 是的, 我们的例子是用来展示的, 但是你应该得到这个理念. 所以读取文件的行的首选方法应该是这样:
f = open('/etc/motd', 'r')
longest = 0
allLines = f.readlines()
f.close()
for line in allLines:
linelen = len(line.strip())
if linelen > longest:
longest = linelen
return longest
列表解析允许我们稍微简化我们代码, 而且我们可以在得到行的集合前做一定的处理. 在下段代码中, 除了读取文件中的行之外,我们还调用了字符串的 strip() 方法处理行内容.
f = open('/etc/motd', 'r')
longest = 0
allLines = [x.strip() for x in f.readlines()]
f.close()
for line in allLines:
linelen = len(line)
if linelen > longest:
longest = linelen
return longest
然而, 两个例子在处理大文件时候都有问题, 因为 readlines() 会读取文件的所有行. 后来我们有了迭代器, 文件本身就成为了它自己的迭代器, 不需要调用 readlines() 函数. 我们已经做到了这一步, 为什么不去直接获得行长度的集合呢(之前我们得到的是行的集合)? 这样, 我们就可以使用 max() 内建函数得到最长的字符串长度:
f = open('/etc/motd', 'r')
allLineLens = [len(x.strip()) for x in f]
f.close()
return max(allLineLens)
这里唯一的问题就是你一行一行迭代 f 的时候, 列表解析需要文件的所有行读取到内存中,然后生成列表. 我们可以进一步简化代码: 使用生成器表达式替换列表解析, 然后把它移到 max()函数里, 这样, 所有的核心部分只有一行:
f = open('/etc/motd', 'r')
longest = max(len(x.strip()) for x in f)
f.close()
return longest
最后, 我们可以去掉文件打开模式(默认为读取), 然后让 Python 去处理打开的文件. 当然,文件用于写入的时候不能这么做, 但这里我们不需要考虑太多:
return max(len(x.strip()) for x in open('/etc/motd'))
我们走了好长一段路. 注意,即便是这只有一行的 Python 程序也不是很晦涩.