迭代器与生成器
Posted lynnlee
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了迭代器与生成器相关的知识,希望对你有一定的参考价值。
迭代器与生成器
一、迭代器
迭代器可以理解为一种特殊的游标,是对循环遍历等一系列操作组成的一种抽象描述。而迭代器协议是程序的一种绑定关系,实现了该协议的对象称为可迭代对象。迭代器协议强调对象必须提供一个next或__next__()方法,并且执行该方法只有两种决策,要么返回迭代中的下一项,要么者引起一个StopIteration异常,以终止迭代。for循环的本质是循环所有对象,使用的一定是迭代器协议生成对象。因此for循环可以遍历所有的可迭代对象(字符串、列表、元组、字典、文件对象等)。既然如此,为什么我们定义一个序列的时候没有使用next方法呢?这是为什么呢?从理论上来讲,只有实现迭代器的对象才可称为可迭代对象。而我们在定义字符串、列表、元组、字典、文件对象的时候,本身没有给出next方法。从这种角度上来看,他们并没有遵循迭代器协议。但是平时我们为什么还是认为他们是可迭代对象呢?
Python提供了一个可以让某种数据类型变为可迭代数据类型的方法,即让某种数据类型的对象直接调用__iter__()或iter()方法,此时我们再查看该数据类型的对象时就多出了next方法。下面如我们通过一个简单的实例来分析,我们使用字符窜调用__iter__()方法,然后使用可迭代对象调用next方法。
string = "hello world" myiter = string.__iter__() print(myiter) print(myiter.__next__()) #输出:h print(myiter.__next__()) #输出:e print(myiter.__next__()) #输出:l print(myiter.__next__()) #输出:l print(myiter.__next__()) #输出:o print(myiter.__next__()) #输出: print(myiter.__next__()) #输出:w print(myiter.__next__()) #输出:o print(myiter.__next__()) #输出:r print(myiter.__next__()) #输出:l print(myiter.__next__()) #输出:d print(myiter.__next__()) #报错:StopIteration异常
结果证明next方法的确是在遍历字符窜,当next方法的遍历越过界时引发StopIteration异常。通过对这个实验的分析,发现for循环这个机制跟索引原来没有半毛钱关系,并且字符串、列表、元组、字典、文件对象本身不遵循迭代器协议,他们能被看作是可迭代对象的原因是因为for循环基于迭代器协议提供了一个统一的可以遍历所有对象的方法,即在遍历之前,先调用对象的__iter__()方法将其转换成一个迭代器,然后用迭代器协议去实现循环访问。,直到for循环捕捉到StopIteration异常终止循环。当然我们也能理解了为什么for循环遍历字典的时候取到是key值,而不是value值,因为它被转化为可迭代对象后直接调用了next方法,而next方法得到的就是key值。包括文件的遍历也是一样的。可迭代对象的好处在于节省内存空间,用即取,不用不取。
二、生成器
基于迭代器而实现的一种数据类型,这种数据类型自动实现了迭代器协议(其他数据类型需要自己调用内置的__iter__()方法),所以生成器本身就具有next方法。不需要通过调用__iter__()方法实现。Python有两种不同的方式提供生成器,下面将进行具体的介绍,当然笔者局限于知识水平不能全方位覆盖生成器的知识点。
1.生成器函数:
常规的函数定义方式,使用yield关键字替代return关键字返回结果。yield关键字一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次从它离开的地方继续执行。yield关键字相当于直接调用迭代器协议,对迭代器进行封装。它的第一个特性是相当于return,第二个特性是可以保留函数的运行状态。那如何保留函数的运行状态呢?我们可以将yield关键字穿插在函数想保留的状态之间。在某种特定的条件下使用next方法依次执行yield以下部分的程序。
def test(): yield "Yahoo" #调用 g = test() print(g) #输出:<generator object test at 0x000001B64793F7D8> print(g.__next__()) #输出:Yahoo
2. 生成器表达式:
类似于列表推导式,它是一种三元运算表达式。生成器按需求返回结果对象,而不是一次性构建一个列表结果。与列表推导式的不同之处在于生成器表达式使用的是一对括号,并且生成器表达式相对于列表推导式更加节省内存。这是为什么呢?因为生成器表达式是基于迭代器而构造的。可迭代对象的好处在于节省内存空间,用即取,不用不取。同时我们也看到Python的创始人对C语言的内存管理是如此的精准。Python的很多内置函数都是基于迭代器生成的。比如map函数、sum函数等等。生成器的应用非常广泛,下面将结合简单的sum函数进行讲解。
num = ("number: %s"%i for i in range(10)) print(num.__next__()) #输出:number: 0 print(num.__next__()) #输出:number: 1 print(num.__next__()) #输出:number: 2 print(num.__next__()) #输出:number: 3 print(num.__next__()) #输出:number: 4
列表非常占用内存,因此我们可以用生成器来替代。Python中的很多内置函数是可迭代的,它们的参数一般传的都是iterable参数。所以如果我们使用生成器来构造一个计算结果,一般来说我们是要在构造语句外面加上一对括号,但是在可迭代的内置函数里面,这对括号可以省略。从上面的知识中我们知道这些内置函数会默认对可迭代参数进行for循环遍历,所以加不加括号是无所谓的。下面程序的输出结果是等效的。
print(sum([1,2,3,4,5,6,7,8,9])) #使用列表 print(sum(i for i in range(10))) #使用生成器 print(sum(i for i in range(10))) #去掉括号 print(sum( range(10) )) #等效替换
3.生成器的总结
生成器函数和普通函数几乎一样。他们之间的差别在于生成器函数使用的是yield关键字返回一个值,而常规函数使用return返回一个值;yield关键字挂起该函数的状态,保留足够的信息,以便之后从它离开的地方继续执行。Python会自动实现迭代器,以便应用到迭代背景中(如很多内置函数)。由于生成器自动实现了迭代器协议。所以,我们可以调用它的next方法,并且在没有值可以返回的时候,生成器自动产生StopIteration异常;生成器的另一个重点知识是生成器只能遍历一次,并且产生生成器的方式除了使用生成器函数的方式外,一个括号也可以产生生成器。
#简单的生成器函数 def test(): for i in range(10): yield i t = test() #调用 for i in t: #第一遍遍历 print(i) #输出列表的内容 tt = (i for i in t) #第二遍遍历 print(list(tt)) #输出:[ ]
#括号产生生成器 def test(): for i in range(10): yield i t = test() #调用 t1 = (i for i in t) t2 = (i for i in t1) print(list(t1)) #输出列表的内容 print(list(t2)) #输出:[ ] """ 这里一直都有一个误区,其实t1和t2都没有任何值,只有在使用内置函数list,list自动 调用next函数的时候,t1的遍开始,于是值才会产生。遍历完成后,由于只能遍历一次, 值都被t1拿走了,t2是取不到值的,因此t2是一个空列表。 """
4.生成器的优点
生成器的好处之一是延迟计算,一次返回一个结果。换言之,它不会一次生成所有的计算结果,这对于大型数据处理非常有用。对内存的开销比较小。生成器的另一个好处是提高代码的可读性。Python编程的思想是在可读性的前提下提炼代码。这一点是Python编程所必须掌握的。下面将应用以上知识模拟单线程并发处理,即生产者与消费者模型。代码如下:
import time def consumer(name): print("【%s】准备吃饺子"%name) while True: dumping = yield time.sleep(1) print("【%s】吃掉%s饺子了"%(name, dumping)) def producer(): p1 = consumer("Lily") p2 = consumer("Jiony") p1.__next__() p2.__next__() for i in range(10): time.sleep(1) p1.send(i) p2.send(i) #调用 producer()
以上是关于迭代器与生成器的主要内容,如果未能解决你的问题,请参考以下文章