day4-python之迭代器和生成器

 

一、引言

有一个列表l = ['a','b','c','d','e'],想取列表中内容有几种方式?

1、索引取值l[0]

2、for循环取值

那么,索引取值和for循环取值的区别是什么。

如果索引取值必须要知道这个值在什么位置。

如果for循环取值,把每个值都取到,不需要关心每个值的位置,因为for循环只能按顺序取值,不能跳过任何一个值直接去取其他位置的值。

但是for循环的内部是怎么工作的呢?为什么可以使用for循环来取值?

二、迭代器

首先,对一个列表进行for循环,肯定是没问题

1 for i in [1,2,3,4]:
2     print(i)

其次,对一个数字1234进行for循环,会报错int类型不是一个iterable(可迭代的)

 1 for i in 1234:
 2     print(i)
 3 
 4 # 结果:
 5 '''
 6 Traceback (most recent call last):
 7   File "C:/Users/benjamin/python自动化21期/day4/00 day4 test.py", line 87, in <module>
 8     for i in 1234:
 9 TypeError: 'int' object is not iterable
10 '''

 

1、什么叫迭代?

将某个数据集内的数据“一个挨着一个的取出来”,就叫做迭代。

 1 from collections import Iterable
 2 
 3 l = [1, 2, 3, 4]
 4 t = (1, 2, 3, 4)
 5 d = {1: 2, 3: 4}
 6 s = {1, 2, 3, 4}
 7 
 8 print(isinstance(l, Iterable))
 9 print(isinstance(t, Iterable))
10 print(isinstance(d, Iterable))
11 print(isinstance(s, Iterable))

字符串、列表、元组、集合、字典都可以被for循环,说明这些都是可迭代的。

2、可迭代协议

可以被迭代要求满足的就叫做可迭代协议。可迭代协议的定义就是内部实现了__iter__方法。

1 print(dir([1,2]))
2 print(dir((2,3)))
3 print(dir({1:2}))
4 print(dir({1,2}))
1 ['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
2 ['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'count', 'index']
3 ['__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']
4 ['__and__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__iand__', '__init__', '__init_subclass__', '__ior__', '__isub__', '__iter__', '__ixor__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__or__', '__rand__', '__reduce__', '__reduce_ex__', '__repr__', '__ror__', '__rsub__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__xor__', 'add', 'clear', 'copy', 'difference', 'difference_update', 'discard', 'intersection', 'intersection_update', 'isdisjoint', 'issubset', 'issuperset', 'pop', 'remove', 'symmetric_difference', 'symmetric_difference_update', 'union', 'update']
结果

可以被for循环的都是可迭代的,要想可迭代,内部必须有一个__iter__方法。

1 print([1,2].__iter__())
2 
3 # 结果:<list_iterator object at 0x0000026D22A59320>

得到一个list_iterator(列表迭代器)

 

3、迭代器协议

1 # dir([1,2]) 是列表中实现的所有方法,dir([1,2].__iter__())是列表迭代器中实现的所有方法,都是以列表的形式返回的。为了看得更清楚,分别将他们转换成集合,然后取差集。
2 # print(dir([1,2]))             # 查看列表的所有方法
3 # print(dir([1,2].__iter__()))  # 查看列表迭代器的所有方法
4 print(set(dir([1,2].__iter__()))-set(dir([1,2])))
5 
6 # 结果:{'__setstate__', '__next__', '__length_hint__'}

三个方法的作用:

 1 iter_l = [1,2,3,4,5,6].__iter__()
 2 
 3 # 获取迭代器中元素的长度
 4 print(iter_l.__length_hint__())
 5 
 6 # 根据索引值指定从哪里开始迭代
 7 print('*',iter_l.__setstate__(4))
 8 
 9 # 一个一个的取值
10 print('**',iter_l.__next__())
11 print('***',iter_l.__next__())

在for循环中,就是在内部调用了__next__方法才能取到一个一个的值。

用迭代器next方法来写一个不依赖for的遍历:

 1 l = [1,2,3,4]
 2 l_iter = l.__iter__()
 3 item = l_iter.__next__()
 4 print(item)
 5 item = l_iter.__next__()
 6 print(item)
 7 item = l_iter.__next__()
 8 print(item)
 9 item = l_iter.__next__()
10 print(item)
11 item = l_iter.__next__()
12 print(item)

这里会抛出一个异常StopIteration,告诉我们列表已经没有有效的元素。

这时,就要使用异常处理机制把这个异常处理掉。

1 l = [1,2,3,4]
2 l_iter = l.__iter__()
3 while True:
4     try:
5         item = l_iter.__next__()
6         print(item)
7     except StopIteration:
8         break

while循环从l_iter获取一个一个的值,那么l_iter就是一个迭代器。

迭代器遵循迭代器协议:必须拥有__iter__方法和__next__方法。

1 print('__next__' in dir(range(12)))  #查看'__next__'是不是在range()方法执行之后内部是否有__next__
2 print('__iter__' in dir(range(12)))  #查看'__next__'是不是在range()方法执行之后内部是否有__next__
3 
4 from collections import Iterator
5 print(isinstance(range(100000000),Iterator))  #验证range执行之后得到的结果不是一个迭代器
range函数的返回值是一个可迭代对象

python2的range不管range多少,会生成一个列表,这个列表将用来存储所有的值(工作全做完才汇报)

python3的range不管range多少,都不会实际的生成任何一个值,只有要的时候才会从迭代器里面生成(每做一步都汇报)

迭代器的优势:

        节省内存

         取一个值就能进行接下来的计算,不需要等到所有值都计算出来才开始接下来的计算 —— 快

迭代器的特性:惰性运算

列表  字典  元组  字符串  集合  range  文件句柄  enumerate

4、为什么要有for循环

有了下标的访问方式,可以这样遍历一个列表:

1 l=[1,2,3]
2 
3 index=0
4 while index < len(l):
5     print(l[index])
6     index+=1

序列类型字符串、列表、元组都有下标,可以用上面方式访问。但是非序列类型字典、集合、文件对象就不可以了。所以,for循环就是基于迭代器协议提供了一个统一的可以遍历所有对象的方法,即在遍历之前,先调用对象的__iter__方法将其转换成一个迭代器,然后使用迭代器协议去实现循环访问,这样所有的对象就都可以通过for循环来遍历。

 三、生成器

 1、迭代器有两种:

1)调用方法直接返回的

2)可迭代对象通过执行iter方法得到的

迭代器的最大好处就是节省内存,而为了节省内存,自己写的能实现迭代器功能的东西就叫做生成器。

2、python中提供的生成器:

1)生成器函数:常规函数定义,但是,使用yield语句而不是return语句返回结果。yield语句一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次从离开的地方继续执行。

2)生成器表达式:类似于列表推导式,但是,生成器返回按需产生结果的一个对象,而不是一次构建一个结果列表

3、生成器Generator:

本质:迭代器(所以自带了__iter__方法和__next__方法,不需要我们去实现)

特点:惰性运算、开发者自定义

 

4、生成器函数

一个包含yield关键字的函数就是一个生成器函数。yield可以为我们从函数中返回值,但是yield又不同于return,return的执行意味着程序的结束,调用生成器函数不会得到返回的具体的值,而是得到一个可迭代的对象。每一次获取这个可迭代对象的值,就能推动函数的执行,获取新的返回值。直到函数执行结果。

 1 import time
 2 def genrator_fun1():
 3     a = 1
 4     print('现在定义了a变量')
 5     yield a
 6     b = 2
 7     print('现在又定义了b变量')
 8     yield b
 9 
10 g1 = genrator_fun1()
11 print('g1 : ',g1)       #打印g1可以发现g1就是一个生成器
12 print('-'*20)   #我是华丽的分割线
13 print(next(g1))
14 time.sleep(1)   #sleep一秒看清执行过程
15 print(next(g1))
初识生成器函数

生成器的好处,就是不会一下子在内存中生成太多数据。

例1:生成器函数,工厂做校服

假如我想让工厂给学生做校服,生产2000000件衣服,我和工厂一说,工厂应该是先答应下来,然后再去生产,我可以一件一件的要,也可以根据学生一批一批的找工厂拿。
而不能是一说要生产2000000件衣服,工厂就先去做生产2000000件衣服,等回来做好了,学生都毕业了。。。

 1 def produce():
 2     """生产衣服"""
 3     for i in range(2000000):
 4         yield "生产了第%s件衣服"%i
 5 
 6 product_g = produce()
 7 print(product_g.__next__()) #要一件衣服
 8 print(product_g.__next__()) #再要一件衣服
 9 print(product_g.__next__()) #再要一件衣服
10 num = 0
11 for i in product_g:         #要一批衣服,比如5件
12     print(i)
13     num +=1
14     if num == 5:
15         break
16 
17 #到这里我们找工厂拿了8件衣服,我一共让我的生产函数(也就是produce生成器函数)生产2000000件衣服。
18 #剩下的还有很多衣服,我们可以一直拿,也可以放着等想拿的时候再拿
初识生成器二
生成器监听文件输入

5、send

 1 def generator():
 2     print(123)
 3     content = yield 1
 4     print('=======',content)
 5     print(456)
 6     yield 2
 7 
 8 g = generator()
 9 ret = g.__next__()
10 print('***',ret)
11 ret = g.send('hello')   #send的效果和next一样
12 rint('***',ret)
13 
14 #send 获取下一个值的效果和next基本一致
15 #只是在获取下一个值的时候,给上一yield的位置传递一个数据
16 #使用send的注意事项
17     # 第一次使用生成器的时候 是用next获取下一个值
18     # 最后一个yield不能接受外部的值

 # 计算移动平均值

# 12  13  15  18

# 月度 的 天平均收入

 1 # 必须先用next再用send
 2 def average():
 3     total=0 #总数
 4     day=0 #天数
 5     average=0 #平均数
 6     while True:
 7         day_num = yield average   #average=0
 8         total += day_num
 9         day += 1
10         average = total/day
11 avg=average() #直接返回生成器
12 next(avg)#激活生成器,avg.send(),什么都不传的时候send和next的效果一样
13 print(avg.send(10))
14 print(avg.send(20))#send   1.传值 2.next
15 print(avg.send(30))
计算移动平均值
 1 # 让装饰器去激活
 2 def wrapper(func):
 3     def inner(*args,**kwargs):
 4        ret = func(*args,**kwargs)
 5        next(ret)
 6        return ret
 7     return inner
 8 
 9 @wrapper
10 def average():
11     total=0 #总数
12     day=0 #天数
13     average=0 #平均数
14     while True:
15         day_num = yield average   #average=0
16         total += day_num
17         day += 1
18         average = total/day
19 
20 
21 ret=average() #直接返回生成器
22 print(ret.send(10))
23 print(ret.send(20))#send   1.传一个值过去 2.让当前yield继续执行
24 print(ret.send(30))
带装饰器的计算移动平均值

 7、yield from

 1 def gen1():
 2     for c in 'AB':
 3         yield c
 4     for i in range(3):
 5         yield i
 6 
 7 print(list(gen1()))
 8 
 9 def gen2():
10     yield from 'AB'
11     yield from range(3)
12 
13 print(list(gen2()))
yield from

 如何从生成器中取值

1、next  随时可以停止,最后一次会报错

print(next(g))

print(next(g))

2、for循环  从头到尾遍历一次,不遇到break,函数里面不遇到return不会停止

for i in g:

  print(i)

3、list、tuple  数据类型的强转,会把所有的数据都加载到内存里,非常浪费内存

print(g)

print(list(g))

 总结:

# 生成器函数 是我们python程序员实现迭代器的一种手段
# 主要特征是 在函数中 含有yield
# 调用一个生成器函数 不会执行这个函数中的带码 只是会获得一个生成器(迭代器)
# 只有从生成器中取值的时候,才会执行函数内部的带码,且每获取一个数据才执行得到这个数据的带码
# 获取数据的方式包括 next send 循环 数据类型的强制转化
# yield返回值的简便方法,如果本身就是循环一个可迭代的,且要把可迭代数据中的每一个元素都返回 可以用yield from
# 使用send的时候,在生成器创造出来之后需要进行预激,这一步可以使用装饰器完成
# 生成器的特点 : 节省内存 惰性运算
# 生成器用来解决 内存问题 和程序功能之间的解耦

四、列表推导式

 1 例1:
 2 # for循环
 3 y = 2
 4 for i in range(100):
 5     print( i * y )
 6 
 7 # 列表推导式
 8 y = 2
 9 l = [ i * y for i in range(100)]
10 print(l)
11 
12 例2:
13 # for循环
14 l=[{'name':'v1','age':'22'},{'name':'v2'}]
15 for dic in l:
16     print(dic['name'])
17 
18 # 列表推导式
19 l=[{'name':'v1','age':'22'},{'name':'v2'}]
20 name_list=[dic['name'] for dic in l]
21 print(name_list)
列表推导式
 1 # ======一层循环======
 2 l = [i*i for i in range(1,10)]
 3 print(l)
 4 # 上面的列表推倒式就相当于下面的
 5 l  = []
 6 for i in range(1,10):
 7     l.append(i*i)
 8 print(l)
 9 l = []
10 
11 
12 # ======多层循环========
13 # 1.列表推倒式
14 l = [i*j for i in range(1,10) for j in range(1,10)]
15 print(l)
16 # 2.循环
17 l = []
18 for i in range(1,10):
19     for j in range(1,10):
20         s = i*j
21         l.append(s)
22 print(l)
列表推导式

 总结:

1)把列表解析的[]换成()得到的就是生成器表达式。

2)列表解析与生成器表达式都是一种便利的编程方式,只不过生成器表达式更节省内存。

3)python不但使用迭代器协议,让for循环变得更加通用。大部分内置函数,也是使用迭代器协议访问对象的。例如,sum函数是python的内置函数,该函数使用迭代器协议访问对象,而生成器实现了迭代器协议,所以,我们可以直接这样计算一系列值的和:

sum(x ** 2 for x in range(4))

而不用多此一举的先构造一个列表:

sum([x ** 2 for x in range(4)]) 

五、推导式详解

列表推导式、生成器表达式、字典推导式、集合推导式等等。

1 variable = [out_exp_res for out_exp in input_list if out_exp == 2]
2   out_exp_res:  列表生成元素表达式,可以是有返回值的函数。
3   for out_exp in input_list:  迭代input_list将out_exp传入out_exp_res表达式中。
4   if out_exp == 2:  根据条件过滤哪些值可以。

1、列表推导式

例1:30以内所有能被3整除的数

1 multiples = [i for i in range(30) if i % 3 is 0]
2 print(multiples)
3 # Output: [0, 3, 6, 9, 12, 15, 18, 21, 24, 27]
列表推导式1

例2:30以内所有能被3整除的数的平方

1 def squared(x):
2     return x*x
3 multiples = [squared(i) for i in range(30) if i % 3 is 0]
4 print(multiples)
列表推导式2

例3:找到嵌套列表中名字含有两个‘e’的所有名字

1 names = [['Tom', 'Billy', 'Jefferson', 'Andrew', 'Wesley', 'Steven', 'Joe'],
2          ['Alice', 'Jill', 'Ana', 'Wendy', 'Jennifer', 'Sherry', 'Eva']]
3 
4 print([name for lst in names for name in lst if name.count('e') >= 2])  # 注意遍历顺序,这是实现的关键
列表推导式3

2、字典推导式

例1:将一个字典的key和value对调

1 mcase = {'a': 10, 'b': 34}
2 mcase_frequency = {mcase[k]: k for k in mcase}
3 print(mcase_frequency)
字典推导式1

例2:合并大小写对应的value值,将k统一成小写

1 mcase = {'a': 10, 'b': 34, 'A': 7, 'Z': 3}
2 mcase_frequency = {k.lower(): mcase.get(k.lower(), 0) + mcase.get(k.upper(), 0) for k in mcase.keys()}
3 print(mcase_frequency)
字典推导式2

3、集合推导式

例:计算列表中每个值的平方,自带去重功能

1 squared = {x**2 for x in [1, -1, 2]}
2 print(squared)
3 # Output: set([1, 4])
集合推导式

 练习题:

例1:  过滤掉长度小于3的字符串列表,并将剩下的转换成大写字母

例2:  求(x,y)其中x是0-5之间的偶数,y是0-5之间的奇数组成的元祖列表

例3:  求M中3,6,9组成的列表M = [[1,2,3],[4,5,6],[7,8,9]]

1 [name.upper() for name in names if len(name)>3] 
2 [(x,y) for x in range(5) if x%2==0 for y in range(5) if y %2==1] 
3 [row[2] for row in M] 
练习题

六、总结

可迭代对象:
  拥有__iter__方法
  特点:惰性运算
  例如:range(),str,list,tuple,dict,set
迭代器Iterator:
  拥有__iter__方法和__next__方法
  例如:iter(range()),iter(str),iter(list),iter(tuple),iter(dict),iter(set),reversed(list_o),map(func,list_o),filter(func,list_o),file_o
生成器Generator:
  本质:迭代器,所以拥有__iter__方法和__next__方法
  特点:惰性运算,开发者自定义
使用生成器的优点:
1.延迟计算,一次返回一个结果。也就是说,它不会一次生成所有的结果,这对于大数据量处理,将会非常有用。

1 #列表解析
2 sum([i for i in range(100000000)])#内存占用大,机器容易卡死
3 #生成器表达式
4 sum(i for i in range(100000000))#几乎不占内存
列表解析式和生成器表达式

2.提高代码可读性

# 生成器和迭代器

# 迭代器 : iter next
# 可以被for循环 节省内存空间 它没有所谓的索引取值的概念 当前的值和下一个值- 公式
# 生成器和迭代器本质上是一样的
# yield函数
# 执行生成器函数 会得到一个生成器 不会执行这个函数中的代码
# 有几个yield,就能从中取出多少个值
# 生成器表达式
# 生成器表达式也会返回一个生成器 也不会直接被执行
# for循环里有几个符合条件的值生成器就返回多少值
# 每一个生成器都会从头开始取值,当取到最后的时候,生成器中就没有值了
# 一个生成器只能用一次
 1 def fff():
 2     for i in range(10):
 3         yield i
 4 g2 = (i**i for i in range(20))
 5 g = fff()
 6 print(next(g))
 7 print(next(g))
 8 print(next(g))
 9 
10 print(next(fff()))
11 print(next(fff()))
12 print(next(fff()))
13 for i in fff():
14     print(i)

七、生成器相关面试题

 1 def demo():
 2     for i in range(4):
 3         yield i
 4 
 5 g=demo()
 6 
 7 g1=(i for i in g)
 8 g2=(i for i in g1)
 9 
10 print(list(g1))
11 print(list(g2))
面试题1

 1 def add(n,i):
 2     return n+i
 3 
 4 def test():
 5     for i in range(4):
 6         yield i
 7 
 8 g=test()
 9 for n in [1,10]:
10     g=(add(n,i) for i in g)
11 
12 print(list(g))
面试题2
 1 import os
 2 
 3 def init(func):
 4     def wrapper(*args,**kwargs):
 5         g=func(*args,**kwargs)
 6         next(g)
 7         return g
 8     return wrapper
 9 
10 @init
11 def list_files(target):
12     while 1:
13         dir_to_search=yield
14         for top_dir,dir,files in os.walk(dir_to_search):
15             for file in files:
16                 target.send(os.path.join(top_dir,file))
17 @init
18 def opener(target):
19     while 1:
20         file=yield
21         fn=open(file)
22         target.send((file,fn))
23 @init
24 def cat(target):
25     while 1:
26         file,fn=yield
27         for line in fn:
28             target.send((file,line))
29 
30 @init
31 def grep(pattern,target):
32     while 1:
33         file,line=yield
34         if pattern in line:
35             target.send(file)
36 @init
37 def printer():
38     while 1:
39         file=yield
40         if file:
41             print(file)
42 
43 g=list_files(opener(cat(grep('python',printer()))))
44 
45 g.send('/test1')
46 
47 协程应用:grep -rl /dir
tail&grep
# 一个生成器 只能取一次
# 生成器在不找它要值的时候始终不执行
# 当他执行的时候,要以执行时候的所有变量值为准
原文地址:https://www.cnblogs.com/gao-dong/p/8993375.html