Python成长之路【第六篇】:Python基础之迭代器和生成器
一、递归和迭代
递归就是调用自身
迭代就是每次迭代都是依赖于上次结果
二、什么是迭代器协议
1、迭代器协议是指:对象必须提供一个next方法,执行该方法要么返回迭代中的下一项,要么就引起一个Stopiteration异常,以终止迭代(只能往后走,不能往前退)
2、可迭代对象:实现了迭代器协议的对象(如何实现:对象内部定义一个__iter__()方法)
3、协议是一种约定,可迭代对象实现了迭代器协议,Python的内部工具(如for循环,sum,min,max等函数)都是使用迭代器协议访问对象
三、Python中强大的for循环机制
for循环的本质:循环所有对象,全都使用的迭代器协议
正本清源:
很多人会想,for循环的本质就是遵循迭代器协议去访问对象,那么for循环的对象肯定就都是迭代器了啊,没错,那既然这样,for循环可以遍历(字符串,列表,元组,字典,集合,文件对象)那这些类型的数据肯定都是可迭代对象?但是,我为什么定义一个列表 a = [1, 2, 3, 4]没有a.next()方法,打脸么
(字符串,列表,元组,字典,集合,文件对象)这些都不是可迭代对象,只不过在for循环时,调用了他们内部的__iter__方法,把他们变成了可迭代对象,然后for循环调用可迭代对象的__next__方法去取值,而且for循环会捕捉Stopiteration异常,以终止迭代
l = ["a", "b", "c"] # 一、下标访问式 print(l[0]) print(l[1]) print(l[2]) # print(l[3]) # 超出边界报错:IndexError # 二、遵循迭代器协议访问方式 diedai_l = l.__iter__() print(diedai_l.__next__()) print(diedai_l.__next__()) print(diedai_l.__next__()) # print(diedai_l.__next__())# 超出边界报错:Stopiteration # 三、for循环访问方式 # for循环 l 本质就是遵循迭代器协议的访问方式,先调用diedai_l = l.__iter__()方 # 法,或者直接diedai_l = iter(l),然后依次执行diedai_l.next(),直到for循环捕捉到 # Stopiteration终止循环,for循环所有对象的本质都是一样的原理 for i in l: # diedai_l = l.__iter__() print(i) # i = diedai_l.next() # 四、用while去模拟for循环做的事情 diedai_l = l.__iter__() while True: try: print(diedai_l.__next__()) except StopIteration: print("迭代完毕了,循环终止了") break
四、为什么要有for循环
基于上面讲的列表的三种访问方式,聪明的你立马就看出了端倪,于是你不知死活的大声喊道,你这不逗我玩呢么,有了下标的访问方式,我可以这样遍历一个列表啊
l = [1, 2, 3] index = 0 while index < len(l): print(l[index]) index += 1 # 要毛线for循环,要毛线for循环,要毛线for循环
没错,序列类型字符串,列表,元组,都有下标,你用上述的方式去范文,perfect!但是你可曾想过非序列类型,像字典,集合,文件对象的感受,所以嘛,年轻人,for循环就是基于迭代器协议提供了一个统一的可以遍历所有对象的方法,既在遍历之前,先调用对象的__iter__方法将其转换成一个迭代器,然后使用迭代器协议去实现循环访问,这样所有的对象就可以通过for循环来遍历了,而且你看到的效果也确实如此,这就是无所不能的for循环,觉悟吧,年轻人
五、生成器初探
什么是生成器?
可以理解为一种数据类型,这种数据类型自动实现了迭代器协议(其他的数据类型需要调用自己内置的__iter__方法),所以生成器就是可迭代对象
生成器分类及在Python中的表现方式:(Python有两种不同的方式提供生成器)
1、生成器函数:用常规函数定义,但是,使用yield语句而不是return语句返回结果。yield语句一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次从它离开的地方继续执行
2、生成器表达式:类似于列表推导,但是,生成器返回按需产生结果的一个对象,而不是一次构建成一个结果列表
为什么使用生成器之生成器的优点
Python使用生成器对延迟操作提供了支持,所谓延迟操作,是指在需要的时候才产生结果,而不是立即产生结果,这也是生成器的主要好处
生成器小结
1、是可迭代对象
2、实现了延迟计算,省内存
3、生成器本质和其他的数据类型一样,都是实现了迭代器协议,只不过生成器附加了一个延迟计算省内存的好处,其余的可迭代对象可没有这点好处
1 a = (i for i in range(10000)) 2 3 # a 就相当于一个生成器 4 5 print(a.__next__()) 6 print(a.__next__()) 7 print(a.__next__()) 8 print(a.__next__()) 9 10 # 用生成器中的next方法取值,可以根据自己的需求来决定,取值的多少,不会像列表那样直接把所有的内容都加载到内存,从而导致机器崩溃
六、生成器表达式和列表解析
# 列表解析:能够简化我们对列表内元素逐一进行操作,比使用普通方法的速度几乎快1倍 egg_list = ["鸡蛋%s" %i for i in range(10)] # 列表解析 print(egg_list) # 一旦输出,便会全部拿出 # 生成器表达式:省内存 egg_list1 = ("鸡蛋%s" %i for i in range(10)) # 生成器表达式 print(egg_list1) # 生成器对象 print(next(egg_list1)) # 用生成器中的next方法取值,next本身调用的就是__next__方法 print(egg_list1.__next__()) # 用生成器中的__next__方法取值 print(next(egg_list1)) # 用生成器中的next方法取值,next本身调用的就是__next__方法
1 name = "小明" 2 name = "小红" 3 4 a = "男的" if name == "小明" else "女的" 5 print(a)
1 l1 = ["鸡蛋%s"%i for i in range(10) if i < 6] 2 3 print(l1)
总结:
1、把列表解析的[]换成()得到的就是生成器表达式
2、列表解析与生成器表达式都是一种便利的编程方式,只不过生成器表达式更节省内存
3、Python不但使用迭代器协议,让for循环变得更加通用,大部分内置函数,也是使用迭代器协议访问对象的。例如:sum函数是Python的内置函数,该函数使用迭代器协议访问对象,而生成器实现了迭代器协议,所以,我们可以直接这样计算一系列值的和:
sum(x ** 2 for x in range(4))
而不用多此一举的先构造一个列表:
sum([x ** 2 for x in range(4)])
七、生成器总结
综上所述,已经对生成器有了一定的认识,下面我们以生成器函数为例进行总结
语法上和函数类似:
生成器函数和常见函数几乎是一样的,他们都是使用def语句进行定义,差别在于,生成器使用yield语句返回一个值,然后挂起状态,而常规函数使用return语句返回一个值后,直接就结束函数了
自动实现迭代器协议:
对于生成器,Python会自动实现迭代器协议,以便应用到迭代背景中(如for循环,sum函数)由于生成器自动实现了迭代器协议,所以,我们可以调用它的next方法,并且,在没有值可以返回的时候,生成器自动产生Stopiteration异常
状态挂起:
生成器使用yield语句返回一个值,yield语句挂起该生成器函数的状态,保留足够的信息,以便之后从它离开的地方继续执行
优点一:
生成器的好处是延迟计算,一次返回一个结果,也就是说,他不会一次生成所有的结果,这对于大数据批量处理,非常有用
# 列表解析 sum([i for i in range(1000000000000)]) # 内存占用大,机器容易卡死 # 生成器表达式 sum(i for i in range(1000000000000)) # 几乎不占内存
优点二:
生成器还能有效提高代码可读性
1 # 求一段文字中,每个单词出现的位置 2 3 # def index_words(test): 4 # result = [] 5 # if test: 6 # result.append(0) 7 # for index, letter in enumerate(test, 1): 8 # if letter == " ": 9 # result.append(index) 10 # return result 11 # 12 # print(index_words("hello world"))
1 # 求一段文字中,每个单词出现的位置 2 3 # def index_words(test): 4 # if test: 5 # yield 0 6 # for index, letter in enumerate(test, 1): 7 # if letter == " ": 8 # yield index 9 # 10 # g = index_words("print hello world!") 11 # print(g.__next__()) 12 # print(g.__next__()) 13 # print(g.__next__()) 14 # # print(g.__next__()) #报错
这里,至少有两个充分的理由,使用生成器比不使用生成器代码更加清晰:
1、使用生成器以后,代码行数更少。如果想把代码写的Pythonic,在保证代码可读性的前提下,代码行数越少越好
2、不使用生成器的时候,对于每次结果,我们首先看到的是result.append(index),其次,才是index。也就是说,我们每次看到的是一个列表的append操作,只是append的是我们想要的结果。使用生成器的时候,直接yield index,少了列表append操作的干扰,我们一眼就能够看出,代码是要返回index
这个例子充分说明了,合理使用生成器,能够有效提高代码可读性。只要大家完全接受了生成器的理论,理解了yield语句和return语句一样,也是返回一个值。那么,就能够理解为什么使用生成器比不使用生成器要好,能够理解使用生成器真的可以让代码变得清晰易懂。
注意事项:生产器只能遍历一次,就相当于(母鸡一生只能下一定数量的蛋,下完了就死掉了)
人口信息.txt文件内容 {"name":"北京", "population":10} {"name":"南京", "population":100000} {"name":"山东", "population":10000} {"name":"山西", "population":19999} def get_provice_population(filename): with open(filename, "r", encoding="utf8") as f: for line in f: p = eval(line) yield p["population"] gen = get_provice_population("人口信息.txt") # 下面的代码一旦执行,将不会有任何输出,这是因为,生成器只能遍历一次,在我们执行sum语句的时候,就遍历了我们的生成器,当我们再次遍历我们的生成器的时候,将不会有任何记录 all_population = sum(gen) for i in gen: print(i/all_population) # 因此,生成器唯一注意的事项就是:生成器只能遍历一次
生成器函数之生产者与消费者
1 import time 2 3 def chibaozi(name): 4 print("我是%s,我要开始吃包子了"%name) 5 while True: 6 baozi = yield 7 time.sleep(1) 8 print("%s把%s号包子吃了"%(name, baozi)) 9 10 def shengchanzhe(): 11 c1 = chibaozi("小明") 12 c1.__next__() 13 for i in range(10): 14 c1.send(i) 15 16 shengchanzhe()