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都是容器对象:
assert 3 in [1,2,3] assert 4 not in [1,2,3] #list assert 3 in (1,2,3) assert 4 not in (1,2,3) #tuple assert 3 in {1,2,3} assert 4 not in {1,2,3} #set
尽管绝大多数容器都提供了某种方式来获取其中的每一个元素,但这并不是容器本身提供的能力,而是 可迭代对象 赋予了容器这种能力,当然并不是所有的容器都是可迭代的。
三 可迭代对象(iterable)
由上图可以看出很多容器都是可迭代对象,此外还有更多的对象同样也是可迭代对象,比如处于打开状态的files,sockets等等。但凡是可以返回一个 迭代器 的对象都可称之为可迭代对象。
x=[1,2,3] y=iter(x) z=iter(x) print(next(y)) print(next(y)) print(next(z)) print(type(x)) print(type(y)) >>>1 >>>2 >>>1 >>><class 'list'> >>><class 'list_iterator'>
这里 x 是一个可迭代对象,可迭代对象和容器一样是一种通俗的叫法,并不是指某种具体的数据类型,list是可迭代对象,dict是可迭代对象,set也是可迭代对象。 y 和 z 是两个独立的迭代器,迭代器内部持有一个状态,该状态用于记录当前迭代所在的位置,以方便下次迭代的时候获取正确的元素。迭代器有一种具体的迭代器类型,比如 list_iterator , set_iterator 。可迭代对象实现了 __iter__ 和 __next__ 方法(python2中是 next 方法,python3是 __next__ 方法),这两个方法对应内置函数 iter() 和 next() 。 __iter__ 方法返回可迭代对象本身,这使得他既是一个可迭代对象同时也是一个迭代器。
四 迭代器
4.1 概述
迭代器是访问集合内元素的一种方式。迭代器对象从集合的第一个元素开始访问,直到所有的元素都被访问一遍后结束。迭代器不能回退,只能往前进行迭代。这并不是什么很大的缺点,因为人们几乎不需要在迭代途中进行回退操作。迭代器也不是线程安全的,在多线程环境中对可变集合使用迭代器是一个危险的操作。但如果小心谨慎,或者干脆贯彻函数式思想坚持使用不可变的集合,那这也不是什么大问题。对于原生支持随机访问的数据结构(如tuple、list),迭代器和经典for循环的索引访问相比并无优势,反而丢失了索引值(可以使用内建函数enumerate()找回这个索引值,这是后话)。但对于无法随机访问的数据结构(比如set)而言,迭代器是唯一的访问元素的方式。迭代器的另一个优点就是它不要求你事先准备好整个迭代过程中所有的元素。迭代器仅仅在迭代至某个元素时才计算该元素,而在这之前或之后,元素可以不存在或者被销毁。这个特点使得它特别适合用于遍历一些巨大的或是无限的集合,比如几个G的文件,或是斐波那契数列等等。这个特点被称为延迟计算或惰性求值(Lazy evaluation)。迭代器更大的功劳是提供了一个统一的访问集合的接口。只要是实现了__iter__()方法的对象,就可以使用迭代器进行访问。
4.2 迭代器的使用
著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到:
1, 1, 2, 3, 5, 8, 13, 21, 34, ...
#代码1
def fab(max): n, a, b = 0, 0, 1 while n < max: print (b,end=' ') a, b = b, a + b n = n + 1 fab(8) >>>1 1 2 3 5 8 13 21
直接在函数fab(max)中用print打印会导致函数的可复用性变差,因为fab返回None。其他函数无法获得fab函数返回的数列。
代码2
def fab(max): L = [] n, a, b = 0, 0, 1 while n < max: L.append(b) a, b = b, a + b n = n + 1 return L print(fab(8)) >>>[1, 1, 2, 3, 5, 8, 13, 21]
虽然满足了可复用性的需求,但是占用了内存空间,最好不要。
代码3
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() c=fab(8) print(c.next(),end=' ') print(c.next(),end=' ') print(c.next(),end=' ') print(c.next(),end=' ') print(c.next(),end=' ') print(c.next(),end=' ') print(c.next(),end=' ') print(c.next(),end=' ') #迭代器的最后一个值 # print(c.next(),end=' ')#再一次取值报错:StopIteration >>>1 1 2 3 5 8 13 21
通过 c.next() 不断返回数列的下一个数,内存占用始终为常数。迭代器就像一个懒加载的工厂,等到有人需要的时候才给它生成值返回,没调用的时候就处于休眠状态等待下一次调用。每次调用 next() 方法的时候做两件事:(1)为下一次调用 next() 方法修改状态(2)为当前这次调用生成返回结果
五 for element in x的实现
在大多数情况下,我们不会一次次调用next()方法去取值,而是通过 for i in (iterable),
def fab(max): n, a, b = 0, 0, 1 while n < max: yield b #生成器的标志 a, b = b, a + b n = n + 1 print(type(fab(8))) for i in fab(8): print(i,end=' ') >>><class 'generator'> >>>1 1 2 3 5 8 13 21
注意:in后面的对象如果是一个迭代器,内部因为有iter方法才可以进行操作,所以,迭代器协议里面有iter和next两个方法,否则for语句无法应用。
六 生成器
6.1 概述
生成器算得上是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(8): print (n,end=' ') >>>1 1 2 3 5 8 13 21
fab就是一个普通的python函数,它特殊的地方在于函数体中没有 return 关键字,函数的返回值是一个生成器对象。当执行 f=fib(8) 返回的是一个生成器对象,此时函数体中的代码并不会执行,只有显示或隐示地调用next的时候才会真正执行里面的代码。yield 的作用就是把一个函数变成一个 generator,带有 yield 的函数不再是一个普通函数,Python 解释器会将其视为一个 generator,在 for 循环执行时,每次循环都会执行 fab 函数内部的代码,执行到 yield b 时,fab 函数就返回一个迭代值,下次迭代时,代码从 yield b 的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到 yield。看起来就好像一个函数在正常执行的过程中被 yield 中断了数次,每次中断都会通过 yield 返回当前的迭代值。
也可以手动调用 fab(8) 的 next() 方法(因为 fab(8) 是一个 generator 对象,该对象具有 next() 方法),这样我们就可以更清楚地看到 fab 的执行流程:
f=fab(8) print(f.__next__(),end=' ') print(f.__next__(),end=' ') print(f.__next__(),end=' ') print(f.__next__(),end=' ') print(f.__next__(),end=' ') print(f.__next__(),end=' ') print(f.__next__(),end=' ') print(f.__next__(),end=' ') # print(f.__next__(),end=' ') >>>1 1 2 3 5 8 13 21 当最后一行没有注释时,报错:StopIteration
6.2 生成器的创建
第一种方法:
生成器表达式: 同列表解析语法,只不过把列表解析的[]换成()
生成器表达式能做的事情列表解析基本都能处理,只不过在需要处理的序列比较大时,列表解析比较费内存。
gen = (x**2 for x in range(5)) print('gen的数据类型:',gen) l=[0,1,2,3,4,5] print('l的数据类型:',type(l)) for g in gen: print(g,end=' ') for x in l : print(x, end='-') >>>gen的数据类型: <generator object <genexpr> at 0x0340A990> >>>l的数据类型: <class 'list'> >>>0 1 4 9 16 0-1-2-3-4-5-
第二种方法:
生成器函数: 在函数中如果出现了yield关键字,那么该函数就不再是普通函数,而是生成器函数。但是生成器函数可以生产一个无限的序列,这样列表根本没有办法进行处理。yield 的作用就是把一个函数变成一个 generator,带有 yield 的函数不再是一个普通函数,Python 解释器会将其视为一个 generator。下面为一个可以无穷生产奇数的生成器函数:
def odd(): n=1 while True: yield n n+=2 odd_num = odd() count = 0 a=input('please input your number:') a=int(a) for o in odd_num: if count >=a: break print(o,end=' ') count +=1 >>>please input your number:5 >>>1 3 5 7 9
6.3 生成器支持的方法
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,可以通过断点来观察 >>>ok >>>7 >>>None >>>8
close():手动关闭生成器函数,后面的调用会直接返回StopIteration异常。
def g4(): yield 1 yield 2 yield 3 g=g4() print(next(g)) #g.close() #print(next(g) ) #关闭后,yield 2和yield 3语句将不再起作用 #后两句注释之前的输出为: >>>1 #注释之后的输出为: >>>1 >>>StopIteration
协程应用:所谓协同程序也就是是可以挂起,恢复,有多个进入点。其实说白了,也就是说多个函数可以同时进行,可以相互之间发送消息等。
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
参考:https://www.cnblogs.com/yuanchenqi/articles/5769491.html