Python——迭代器&生成器
一 概要
在了解Python的数据结构时,容器(container)、可迭代对象(iterable)、迭代器(iterator)、生成器(generator)、列表/集合/字典推导式(list,set,dict comprehension)众多概念参杂在一起,难免让初学者一头雾水,我将用一篇文章试图将这些概念以及它们之间的关系捋清楚

二 容器(container)
容器是一种把多个元素组织在一起的数据结构,容器中的元素可以逐个地迭代获取,可以用 in , not in 关键字判断元素是否包含在容器中。通常这类数据结构把所有的元素存储在内存中(也有一些特列并不是所有的元素都放在内存)在Python中,常见的容器对象有:
- list, deque, ....
- set, frozensets, ....
- dict, defaultdict, OrderedDict, Counter, ....
- tuple, namedtuple, …
- str
容器比较容易理解,因为你就可以把它看作是一个盒子、一栋房子、一个柜子,里面可以塞任何东西。从技术角度来说,当它可以用来询问某个元素是否包含在其中时,那么这个对象就可以认为是一个容器,比如 list,set,tuples都是容器对象:

尽管绝大多数容器都提供了某种方式来获取其中的每一个元素,但这并不是容器本身提供的能力,而是 可迭代对象 赋予了容器这种能力,当然并不是所有的容器都是可迭代的。
三 可迭代对象(iterable)
如果给定一个list或tuple,我们可以通过for循环来遍历这个list或tuple,这种遍历我们称为迭代(Iteration)。
了解迭代器,必须先弄明白什么是迭代。
定义:迭代是重复反馈过程的活动,其目的通常是为了逼近所需目标或结果。每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值(以region growing为例)。
那么下面的while循环的例子是否是否是一个迭代的过程呢?
count=0
while count<10:
print(“oldboy”)
count+=1
答案是否定的。因为这个过程只是满足了迭代的第一个条件:重复,而并没有以每次的结果作为下次的初始值,而这才是迭代的关键。
l=[‘a’,’b’,’c’,’d’,’e’]
index=0
while index < len(l):
print(l[index])
index+=1
这次while循环才是一个迭代过程,不仅满足重复,而且以每次重新赋值后的index值作为下一次循环中新的索引进行取值,反复迭代,最终可以得到所有的列表中的值。通过索引的方式进行迭代取值,实现简单,但适用序列类型,如字符串,列表,元组。对于没有索引的:字典,集合等非序列类型,以及用户自定义的对象,这种按索引迭代取值的方式便不再适用。
刚才说过,很多容器都是可迭代对象,此外还有更多的对象同样也是可迭代对象,比如处于打开状态的files,sockets等等。但凡是可以返回一个 迭代器 的对象都可称之为可迭代对象,听起来可能有点困惑,没关系,可迭代对象与迭代器有一个非常重要的区别。先看一个例子:

这里 x 是一个可迭代对象,可迭代对象和容器一样是一种通俗的叫法,并不是指某种具体的数据类型,list是可迭代对象,dict是可迭代对象,set也是可迭代对象。 y 和 z 是两个独立的迭代器,迭代器内部持有一个状态,该状态用于记录当前迭代所在的位置,以方便下次迭代的时候获取正确的元素。迭代器有一种具体的迭代器类型,比如 list_iterator , set_iterator 。可迭代对象实现了 __iter__ 和 __next__ 方法(python2中是 next 方法,python3是 __next__ 方法),这两个方法对应内置函数 iter() 和 next() 。 __iter__ 方法返回可迭代对象本身,这使得他既是一个可迭代对象同时也是一个迭代器。
在具体讲迭代器之前,先了解一个概念:可迭代对象(Iterable)。之前在数据类型中介绍的容器对象(列表,元组,字典,集合等)都是可迭代对象;从语法形式上讲,能调用__iter__方法的数据对象就是可迭代对象:
>>> [1,2,3].__iter__()
<listiterator object at 0x10221b150>
>>> {'name':'alvin'}.__iter__()
<dictionary-keyiterator object at 0x1022180a8>
>>> {7,8,9}.__iter__()
<setiterator object at 0x1021ff9b0>
obj.__iter__()方法调用后返回的就是一个迭代器对象(Iterator)。迭代器对象的特性就是能够调用__next__方法依次计算出迭代器中的下一个值。基于此就可以实现无论是否数据为序列对象,都可以通过迭代取值的方式完成查询功能。
>>> s={1,2,3}
>>> i=s.__iter__() # 返回可迭代对象s的迭代器对象i
>>> i.__next__() # 从第一个元素开始,i通过__next__方法就可以得到可迭代对象s的下一个值。
1
>>> i.__next__()
2
>>> i.__next__()
3
>>> i.__next__() #迭代结束,没有下一个值时调用__next__()抛出StopIteration的异常
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
解析:针对一个没有索引的可迭代数据类型,我们执行s.__iter__()方法便得到一个迭代器,每执行一次i.__next__()就获取下一个值,待所有值全部取出后,就会抛出异常StopIteration,不过这并不代表错误发生,而是一种迭代完成的标志。需要强调的是:此处我们迭代取值的过程,不再是通过索引而是通过__next__方法。
提示:可以用iter(s)取代s._iter__(),其实iter(s)本质就是在调用s.__iter__(),这与len(s)会调用s.__len__()是一个原理,同理,可以用next(i)取代i.__next__()。obj.__iter__()方法的调用后返回的就是一个迭代器对象
于是我们我们可以将之前基于索引的迭代换成迭代器的形式,如下:
#while的形式
l=['a','b','c','d','e']
i=iter(l)
while 1:
try:
print(next(i))
except StopIteration:
break
解析:获取迭代器赋值给i,然后循环内用try监测print语句可能发生的异常,每次执行next(i)获取一个值并打印,直到值完全取尽则抛出异常StopIteration被except捕捉到,执行break跳出循环。
四 迭代器(iterator)
那么什么迭代器呢?它是一个带状态的对象,他能在你调用 next() 方法的时候返回容器中的下一个值,任何实现了 __next__() (python2中实现 next() )方法的对象都是迭代器,至于它是如何实现的这并不重要。
现在我们就以斐波那契数列()为例,学习为何创建以及如何创建一个迭代器:
著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到:
1, 1, 2, 3, 5, 8, 13, 21, 34, ...
1 def fab(max): 2 n, a, b = 0, 0, 1 3 while n < max: 4 print b 5 a, b = b, a + b 6 n = n + 1
直接在函数fab(max)中用print打印会导致函数的可复用性变差,因为fab返回None。其他函数无法获得fab函数返回的数列。
1 def fab(max): 2 L = [] 3 n, a, b = 0, 0, 1 4 while n < max: 5 L.append(b) 6 a, b = b, a + b 7 n = n + 1 8 return L
代码2满足了可复用性的需求,但是占用了内存空间,最好不要。
对比for i in range(1000): pass和for i in xrange(1000): pass,前一个返回1000个元素的列表,而后一个在每次迭代中返回一个元素,因此可以使用迭代器来解决复用可占空间的问题
1 class Fab(object): 2 def __init__(self, max): 3 self.max = max 4 self.n, self.a, self.b = 0, 0, 1 5 6 def __iter__(self): 7 return self 8 9 def next(self): 10 if self.n < self.max: 11 r = self.b 12 self.a, self.b = self.b, self.a + self.b 13 self.n = self.n + 1 14 return r 15 raise StopIteration() 16 17 18 ''' 19 >>> for key in Fabs(5): 20 print key 21 1 22 3 23 ''' 24 25 Code3
Fabs 类通过 next() 不断返回数列的下一个数,内存占用始终为常数
Fib既是一个可迭代对象(因为它实现了 __iter__ 方法),又是一个迭代器(因为实现了 __next__ 方法)。实例变量 self .a 和 self.b 用户维护迭代器内部的状态。每次调用 next() 方法的时候做两件事:
- 为下一次调用 next() 方法修改状态
- 为当前这次调用生成返回结果
迭代器就像一个懒加载的工厂,等到有人需要的时候才给它生成值返回,没调用的时候就处于休眠状态等待下一次调用。
五 for i in (iterable)的内部实现
在大多数情况下,我们不会一次次调用next方法去取值,而是通过 for i in (iterable),


注意:in后面的对象如果是一个迭代器,内部因为有iter方法才可以进行操作,所以,迭代器协议里面有iter和next两个方法,否则for语句无法应用。
注意:
|
1
2
|
for i in range(10): print i :定时垃圾回收机制:没有引用指向这个对象,则被回收 |
六 你不了解的for循环
之前的学习只知道for循环是用来遍历某个数据对象的。但for循环内部到底是怎么工作的,关键字in后面可以放什么数据类型呢?让我们带着这些疑问一起去解析for循环的实现机制。
#for循环的形式:
for val in obj:
print(val)
解析:关键字in后面数据对象必须是可迭代对象。for 循环首先会调用可迭代对象内的__iter__方法返回一个迭代器,然后再调用这个迭代器的next方法将取到的值赋给val,即关键字for后的变量。循环一次,调用一次next方法,直到捕捉StopIteration异常,结束迭代。解析:关键字in后面数据对象必须是可迭代对象。for 循环首先会调用可迭代对象内的__iter__方法返回一个迭代器,然后再调用这个迭代器的next方法将取到的值赋给val,即关键字for后的变量。循环一次,调用一次next方法,直到捕捉StopIteration异常,结束迭代。
l=[11,22,33]
for i in l: #调用iter方法返回一个关于[11,22,33]的迭代器
print(i) #迭代器调用next方法返回的值赋值给i,即i=next(iter(l))
#执行结果
11
22
33
七 迭代器的优缺点
对比之前基于索引实现迭代的方式,所有迭代的值都保存到内存中(每次迭代都是基于索引来取值),而迭代器方式则是需要一个值时,通过调用__next__方法临时计算出,然而这种处理数据的方式优点是很明显的:
1、为序列和非序列对象提供了一种统一的迭代方式。
2、惰性计算:python中的Iterator对象只是在需要时才去不断调用next()来计算一个个值,没错,就是计算,是你需要时,临时计算出,之前是不存在的。就迭代器本身来说,同一时刻在内存中只有一个值,因而可以存放无限大的数据流,而对于其他容器类型,如列表,需要把所有的元素都存放于内存中,受内存大小的限制,可以存放的值的个数是有限的。
比如自定义的斐波那契数列迭代器,无论查询第几位数的值,在内存中的开销都是有限的:
class Fab(object):
def __init__(self, max):
self.max = max
self.n, self.a, self.b = 0, 0, 1
def __iter__(self):
return self
def next(self):
if self.n < self.max:
r = self.b
self.a, self.b = self.b, self.a + self.b #这次结果作为下次的初始值
self.n = self.n + 1
return r
raise StopIteration()
'''
>>> for key in Fabs(5):
print key
1
1
2
3
5
'''
刚才说过,很多容器都是可迭代对象,此外还有更多的对象同样也是可迭代对象,
比如处于打开状态的文件:
f=open("a.txt")
# 调用f的iter方法,返回一个关于这个文件的迭代器对象,对其调用next方法得到一行内容赋值给i
for i in f: #
print(i.strip())
相比于for line in f.readlines()形式将文件所有内容存到内存,迭代器的形式会明显节省内存,效率更高。
迭代器使用是有限制的:
(1)只能一个一个地向后移动且无法预知长度
(2)不能回到开始
(3)无法复制一个迭代器
l=iter([11,22,33])
for i in l:
print(i)
# 如果有两个或者多个循环使用同一个迭代器,必然只会有一个循环能取到值。
for i in l: # l已经迭代结束,在这里为空,所以这次for循环不会执行。
print("i:",i)
#执行结果
11
22
33
迭代器由于对内存存储的优化在py3中已经大量使用。
另外,我们可以通过isinstance()判断一个对象是否是Iterable和Iterator:
>>> from collections import Iterable,Iterator
>>> isinstance([1,2,3],Iterable)
True
>>> i=iter('abc')
>>> isinstance(i,Iterator)
True
八 迭代器协议
迭代器协议要求迭代对象具有__iter__()和__next__()两个方法,__next__之前讲过,是用于计算下一个值的,而__iter__则是返回迭代器本身,目的是使for循环可以遍历迭代器对象,for循环的本质是调用被迭代对象内部的__iter__方法将其变成一个迭代器然后进行迭代取值的操作,如果对象没有__iter__方法则会报错。所以可以说,迭代器对象都是可迭代对象就是因为其内部定义了__iter__方法。
这里自定义一个迭代器对象作为了解:pass
九 生成器(generator)
生成器算得上是Python语言中最吸引人的特性之一,生成器其实是一种特殊的迭代器,不过这种迭代器更加优雅。代码3远没有代码1简洁,生成器(yield)既可以保持代码1的简洁性,又可以保持代码3的效果。它不需要再像上面的类一样写 __iter__() 和 __next__() 方法了,只需要一个 yiled 关键字。 生成器有如下特征是它一定也是迭代器(反之不成立),因此任何生成器也是以一种懒加载的模式生成值。用生成器来实现斐波那契数列的例子是:
def fab(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
>>> for n in fab(5):
print n
1
1
2
3
5
fib 就是一个普通的python函数,它特需的地方在于函数体中没有 return 关键字,函数的返回值是一个生成器对象。当执行 f=fib(5) 返回的是一个生成器对象,此时函数体中的代码并不会执行,只有显示或隐示地调用next的时候才会真正执行里面的代码。
yield 的作用就是把一个函数变成一个 generator,带有 yield 的函数不再是一个普通函数,Python 解释器会将其视为一个 generator,在 for 循环执行时,每次循环都会执行 fab 函数内部的代码,执行到 yield b 时,fab 函数就返回一个迭代值,下次迭代时,代码从 yield b 的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到 yield。看起来就好像一个函数在正常执行的过程中被 yield 中断了数次,每次中断都会通过 yield 返回当前的迭代值。
也可以手动调用 fab(5) 的 next() 方法(因为 fab(5) 是一个 generator 对象,该对象具有 next() 方法),这样我们就可以更清楚地看到 fab 的执行流程:
>>> f = fab(3)
>>> f.__next__()
1
>>> f.__next__()
1
>>> f.__next__()
2
>>> f.__next__()
Traceback (most recent call last):
File "<pyshell#62>", line 1, in <module>
f.next()
StopIteration
需要明确的就是生成器也是iterator迭代器,因为它遵循了迭代器协议.
两种创建方式
包含yield的函数
生成器函数跟普通函数只有一点不一样,就是把 return 换成yield,其中yield是一个语法糖,内部实现了迭代器协议,同时保持状态可以挂起。如下:
return:
在一个生成器中,如果没有return,则默认执行到函数完毕;如果遇到return,如果在执行过程中 return,则直接抛出 StopIteration 终止迭代.
def f(): yield 5 print("ooo") return yield 6 print("ppp") # if str(tem)=='None': # print("ok") f=f() # print(f.__next__()) # print(f.__next__()) for i in f: print(i) ''' return即迭代结束 for不报错的原因是内部处理了迭代结束的这种情况 '''
注意:
def read_file(fpath): BLOCK_SIZE = 1024 with open(fpath, 'rb') as f: while True: block = f.read(BLOCK_SIZE) if block: yield block else: return
如果直接对文件对象调用 read() 方法,会导致不可预测的内存占用。好的方法是利用固定长度的缓冲区来不断读取文件内容。通过 yield,我们不再需要编写读文件的迭代类,就可以轻松实现文件读取。
My:生成器对象就是一种特殊的迭代器对象,满足迭代器协议,可以调用next;对生成器对象for 循环时,调用iter方法返回了生成器对象,然后再不断next迭代,而iter和next都是在yield内部实现的。
看下面例子:
>>> def countdown(n):
... print('countdown start')
... while n > 0:
... yield n
... n-=1
... print('Done!')
>>> countdown
<function countdown at 0x102212f50>
>>> countdown(5)
<generator object countdown at 0x1021ff9b0>
>>> gen=countdown(3) # 验证对象是否为迭代器对象
>>> gen
<generator object countdown at 0x101be0a40>
>>> from collections import Iterator
>>> isinstance(gen,Iterator)
True
countdown是一个函数名,但是调用函数countdown(5)时,并没有像函数一样执行函数体,而是返回了一个生成器对象(generator object)。
生成器本质就是一个迭代器,也可以调用__iter__和__next__方法,只不过这两个方法是由yield关键字在函数调用时封装好的,不用再自己定义,所以,生成器也被称为优雅的迭代器。
>>> gen=countdown(3)
>>> gen.__next__()
countdown start
3
>>> gen.__next__()
2
>>> gen.__next__()
1
>>> gen.__next__()
Done!
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
解析:生成器函数调用时只会返回一个生成器对象。只有当生成器对象调用__next__方法时才会触发函数体代码执行,直到遇到关键字yield停止,将yield后的值作为返回值返回,所以,yield类似于return的功能,但不同于return的是,return返回,函数结束;而yield将函数的状态挂起,等待生成器对象再次调用__next__方法时,函数从挂起的位置后的第一条语句继续运行直到再遇见yield并返回其后的值;如果不断调用__next__方法,最后一次进入函数体,待执行代码不再有yield此时报出迭代异常的错误。
既然生成器对象属于迭代器,那么必然可以使用for循环迭代:
>>> for i in countdown(3):
... print(i)
...
countdown start
3
2
1
Done!
对比迭代器,生成器对象新增一个内置的close方法用来关闭自己,如下:
>>> c=countdown(7)
>>> c
<generator object countdown at 0x10123f308>
>>> next(c)
countdown start
7
>>> next(c)
6
>>> c.close()
>>> next(c)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
解析:首先获取生成器对象c,然后执行两次next后,调用c.close()关闭自身,这意味着虽然没有迭代到最后一次也不能再通过c取下一个值,所以next(c)抛出迭代结束异常。
yield的功能总结:
(1)封装iter和next方法
(2)执行函数时遇到yield返回其后的值,不同于return,yiled可以返回多次值
(3)挂起函数的状态,等待下一次调用next方法时找到对应的暂停位置继续执行。
十 生成器表达式
创建一个生成器对象有两种方式,一是通过在函数中创建yield关键字来实现。另一种就是生成器表达式,这是一种类似于数据类型中学过的列表生成式的语法格式,只是将[]换成(),即:
(expression for item in iterable if condition)
不同于列表生成式最后返回一个列表结果,生成器表达式顾名思义会返回一个生成器对象,比如:
>>> [x*x for x in range(4)] #
[0, 1, 4, 9]
>>> gen=(x*x for x in range(4))
>>> gen
<generator object <genexpr> at 0x101be0ba0>
当需要用到其中的值时,再通过调用next方法或者for循环将值一个个地计算出来:
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
4
>>> next(gen)
9
>>> next(gen)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
#------------for 循环------------------
>>> gen=(x*x for x in range(4))
>>> for i in gen:
... print(i)
...
0
1
4
9
优点自然是节省内存,一次只产生一个值在内存中。举一个简单的应用:查找某文件中最长的行的长度。简单实现:
def f():
f = open('FILENAME', 'r')
longest = 0
while True:
linelen = len(f.readline().strip())
if not linelen:
break
if linelen > longest:
longest = linelen
f.close()
return longest
存在问题:比如一个日志文件,多个进程都想操作它,所以尽量不要让一个程序占用这个文件句柄太久的时间,改进如下:
def f1():
f = open('FILENAME', 'r')
allLinesLen = [line(x.strip()) for x in f]
f.close()
return max(allLinesLen)
最后一个问题,即对于每一行处理完生成的数据会全部加载到内存,大量占用内存空间,所以可以用生成器表达式替换:
f = open('FILENAME', 'r')
longest = max(line(x.strip()) for x in f)
f.close()
return longest
那是不是写成一句话更简单呢?
return max(line(x.strip()) for x in open('FILENAME'))
虽然更简单,但并不推荐这样写,因为这样每次循环都需要重新执行open()函数,会降低运行效率。
练习1:使用文件读取,找出文件中最长的行的?
max(len(x.strip()) for x in open('/hello/abc','r'))
练习2:
def add(s, x):
return s + x
def gen():
for i in range(4):
yield i
base = gen()
for n in [1, 10]:
base = (add(i, n) for i in base)
print list(base)
''' 核心语句就是: for n in [1, 10]: base = (add(i, n) for i in base) 在执行list(base)的时候,开始检索,然后生成器开始运算了。关键是,这个循环次数是2,也就是说,有两次生成器表达 式的过程。必须牢牢把握住这一点。 生成器返回去开始运算,n = 10而不是1没问题吧,这个在上面提到的文章中已经提到了,就是add(i, n)绑定的是n这个 变量,而不是它当时的数值。 然后首先是第一次生成器表达式的执行过程:base = (10 + 0, 10 + 1, 10 + 2, 10 +3),这是第一次循环的结 果(形象表示,其实已经计算出来了(10,11,12,3)),然后第二次, base = (10 + 10, 11 + 10, 12 + 10, 13 + 10) ,终于得到结果了[20, 21, 22, 23]. '''
练习3:自定义range


十一 生成器的扩展
生成器对象支持几个方法,如gen.next() ,gen.send() ,gen.throw()等。


由于没有额外的yield,所以将直接抛出StopIteration。


send的工作方式:
def f():
print("ok")
s=yield 7
print(s)
yield 8
f=f()
print(f.send(None))
print(next(f))
#print(f.send(None))等同于print(next(f)),执行流程:打印ok,yield7,当再next进来时:将None赋值给s,然后返回8,可以通过断点来观察
协程应用:
所谓协同程序也就是是可以挂起,恢复,有多个进入点。其实说白了,也就是说多个函数可以同时进行,可以相互之间发送消息等。
import queue def tt(): for x in range(4): print ('tt'+str(x) ) yield def gg(): for x in range(4): print ('xx'+str(x) ) yield class Task(): def __init__(self): self._queue = queue.Queue() def add(self,gen): self._queue.put(gen) def run(self): while not self._queue.empty(): for i in range(self._queue.qsize()): try: gen= self._queue.get() gen.send(None) except StopIteration: pass else: self._queue.put(gen) t=Task() t.add(tt()) t.add(gg()) t.run() # tt0 # xx0 # tt1 # xx1 # tt2 # xx2 # tt3 # xx3
参考:
http://anandology.com/python-practice-book/iterators.html
http://www.cnblogs.com/kaituorensheng/p/3826911.html
http://www.jb51.net/article/80740.htm
http://www.open-open.com/lib/view/open1463668934647.html
转自袁老师:http://www.cnblogs.com/yuanchenqi/articles/5769491.html

浙公网安备 33010602011771号