py之路——day13-20230821:生成器和迭代器
作者:zb
一、列表生成式
1、定义
用来生成列表的表达式
2、特点
可以使代码更加简洁
示例代码如下:
1 # 普通方法定义列表 2 a = [1, 2, 3] 3 print(a) 4 # 列表生成式方法定义列表 5 b = [i*2 for i in range(10)] 6 print(b) 7 # 如果不用列表生成式,上述b列表定义会很麻烦 8 c = [] 9 for i in range(10): 10 c.append(i*2) 11 print(c) 12 # 列表生成式还可以调用函数 13 d = [func(i) for i in range(10)] 14 print(d)
二、生成器
1、生成器的定义
生成器是一个对象,并不是一个实际的可迭代对象(例如元组、字典、列表等),它是一种算法,相关的元素可以通过这种算法一一计算出来,但是不循环执行算法时,生成器不会生成对应的元素。
2、生成器的特点:
⑴代码执行速度快,因为生成器只是定义了一个生成器对象,只有在被调用时才会产生实际的数据
示例代码如下:
1 # 列表生成式 2 a = [i*2 for i in range(5)] 3 print(a[1]) 4 # 生成器 5 b = (i*2 for i in range(5)) 6 print(b) 7 # 生成器只有被调用的时候才会生成数据,因此可以通过循环取生成器的值,生成器不可以被切片 8 for i in b: 9 print(i)
执行结果:
1 D:\oldboy_py\venv\Scripts\python.exe D:/oldboy_py/day3-20230524迭代器与生成器/2、生成器.py 2 2 3 <generator object <genexpr> at 0x0000020927D746D0> 4 0 5 2 6 4 7 6 8 8 9 10 Process finished with exit code 0
⑵生成器是一个对象,生成器和列表不同,生成器只是一个对象,并没有存储具体的数据,只有在调用生成器的时候才会返回具体的数据,而列表生成式不一样,列表生成式已经存储好了所有的数据,因此程序运行时会大量占用内存空间
示例代码如下:
1 # 生成器,只有在被调用的时候才会生成数据,否则就是一个对象而已,不占用内存空间 2 a = (i*2 for i in range(10)) 3 print(a) 4 # 列表生成式,执行后就生成了所有的数据,占用内存空间 5 b = [i*2 for i in range(10)] 6 print(b)
执行结果:
1 D:\oldboy_py\venv\Scripts\python.exe D:/oldboy_py/day3-20230524迭代器与生成器/2、生成器.py 2 <generator object <genexpr> at 0x00000232292946D0> 3 [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] 4 5 Process finished with exit code 0
⑶如何调用生成器生成数据?
使用生成器对象的__next__()方法
示例代码如下:
1 # 生成器的__next__()方法 2 a = (i*2 for i in range(100000000)) 3 print(a.__next__()) 4 print(a.__next__())
执行结果:
1 D:\oldboy_py\venv\Scripts\python.exe D:/oldboy_py/day3-20230524迭代器与生成器/2、生成器.py 2 0 3 2 4 5 Process finished with exit code 0
⑷生成器运行过程中是如何节省内存空间的?原理是什么?
生成器只能使用__next__()方法生成数据,而且只会保存当前位置生成的数据,之前的数据会被丢弃掉
⑸生成器不能被切片
3、斐波那契数列
⑴简单的生成器可以使用for循环直接生成
示例代码如下:
1 b = (i*2 for i in range(5)) 2 for i in b: 3 print(i)
执行结果:
1 D:\oldboy_py\venv\Scripts\python.exe D:/oldboy_py/day3-20230524迭代器与生成器/2、生成器.py 2 0 3 2 4 4 5 6 6 8 7 8 Process finished with exit code 0
⑵但是如果生成的算法比较复杂,for循环无法生成,该怎么办呢?
那就要使用函数来定义生成器的算法,例如我们先写一个生成斐波那契数列的函数
示例代码如下:
1 def fib(max): 2 n, a, b = 0, 0, 1 3 while n < max: 4 # 斐波那契数列每次只打印b 5 print(b) 6 #a, b = b, a+b,理论上a=0,b=1,此时应该是a=1,b=1+1=2,所以数列应该是1,2...而不是1,1... 7 #所以这里的a,b=b,a+b这种赋值方法其实是t=(b,a+b),a=t[0],b=t[1],即a=1,b=1,即1,1... 8 a, b = b, a+b 9 n = n+1 10 print('done') 11 fib(10)
执行结果:
1 D:\oldboy_py\venv\Scripts\python.exe D:/oldboy_py/day3-20230524迭代器与生成器/3、斐波那契数列.py 2 1 3 1 4 2 5 3 6 5 7 8 8 13 9 21 10 34 11 55 12 done 13 14 Process finished with exit code 0
那么问题来了,如何将函数变成生成器呢?只需要对代码做如下改动即可
示例代码如下:
1 def fib(max): 2 n, a, b = 0, 0, 1 3 while n < max: 4 yield b 5 a, b = b, a + b 6 n = n + 1 7 f = fib(10) 8 print(f)
执行结果:
1 D:\oldboy_py\venv\Scripts\python.exe D:/oldboy_py/day3-20230524迭代器与生成器/4、将函数变成生成器.py 2 <generator object fib at 0x00000241481A6030> 3 4 Process finished with exit code 0
4、函数式生成器
⑴将函数变为生成器的好处
可以随时停止函数的调用,并穿插着做点别的事情,想继续调用函数时可以继续调用
示例代码如下:
1 def fib(max): 2 n, a, b = 0, 0, 1 3 while n < max: 4 yield b 5 a, b = b, a + b 6 n = n + 1 7 f = fib(10) 8 print(f) 9 print(f.__next__()) 10 print(f.__next__()) 11 print(f.__next__()) 12 print('===干点别的事情===') 13 print(f.__next__()) 14 print(f.__next__())
执行结果:
1 D:\oldboy_py\venv\Scripts\python.exe D:/oldboy_py/day3-20230524迭代器与生成器/4、将函数变成生成器.py 2 <generator object fib at 0x000001B61F586030> 3 1 4 1 5 2 6 ===干点别的事情=== 7 3 8 5 9 10 Process finished with exit code 0
⑵yield
带yield的函数,不能再称之为函数了,它已经是一个生成器了,并且可以通过return关键字返回调用过程中的异常消息(想把谁返回到外部,就把谁定义为yield,yield可以保存函数的这种中断状态)
示例代码如下:
1 def fib(max): 2 """ 3 定义一个斐波那契数列的生成器 4 :param max: 5 :return: 6 """ 7 n,a,b = 0,0,1 8 while n<max: 9 yield b 10 a,b = b,a+b 11 n+=1 12 return "超过了最大循环次数" 13 f = fib(10) 14 15 # 写一个判断异常的代码 16 while True: 17 try: 18 x = next(f) 19 print('f', x) 20 except StopIteration as e: 21 print("运行出错:", e.value) 22 break
执行结果:
1 D:\oldboy_py\venv\Scripts\python.exe D:/oldboy_py/day3-20230524迭代器与生成器/5、函数生成器通过return关键字打印报错信息.py 2 f 1 3 f 1 4 f 2 5 f 3 6 f 5 7 f 8 8 f 13 9 f 21 10 f 34 11 f 55 12 运行出错: 超过了最大循环次数 13 14 Process finished with exit code 0
⑶使用生成器yield实现单线程下的并发
使用生成器yield的特性可以实现同一时间执行多个任务,这种原理在python中叫做协程,协程是比线程更小的单位,存在于线程里,我们熟知的Nginx就是使用了协程的原理,为什么Nginx的执行效率高?原因就是单线程下的异步IO,这样导致Nginx可承载的并发量比多线程还要高好多倍,我们用一段简单的代码来实现协程,生成器的__next__()方法只是用来初始化生成器(即让生成器做好接受传值的准备),而send()方法可以给生成器传值
示例代码如下:
1 import time 2 3 def consumer(name): 4 """ 5 消费者 6 :param name: 7 :return: 8 """ 9 10 while True: 11 12 baozi = yield 13 print("准备吃包子啦!") 14 print("包子[%s]来了,被[%s]吃了!" % (baozi, name)) 15 16 def producer(): 17 """ 18 生产者 19 :return: 20 """ 21 c1 = consumer('A') # 调用生成器 22 c2 = consumer('B') # 调用生成器 23 c1.__next__() # 初始化生成器 24 c2.__next__() # 初始化生成器 25 print("老子准备开始做包子啦!") 26 for i in range(1,11): 27 print("做了一个包子,分两半!") 28 time.sleep(1) 29 c1.send(i) 30 c2.send(i) 31 32 producer()
执行结果:
1 D:\oldboy_py\venv\Scripts\python.exe D:/oldboy_py/day3-20230524迭代器与生成器/6、通过yield(生成器)实现单线程下的并发(即协程).py 2 老子准备开始做包子啦! 3 做了一个包子,分两半! 4 准备吃包子啦! 5 包子[1]来了,被[A]吃了! 6 准备吃包子啦! 7 包子[1]来了,被[B]吃了! 8 做了一个包子,分两半! 9 准备吃包子啦! 10 包子[2]来了,被[A]吃了! 11 准备吃包子啦! 12 包子[2]来了,被[B]吃了! 13 做了一个包子,分两半! 14 准备吃包子啦! 15 包子[3]来了,被[A]吃了! 16 准备吃包子啦! 17 包子[3]来了,被[B]吃了! 18 做了一个包子,分两半! 19 准备吃包子啦! 20 包子[4]来了,被[A]吃了! 21 准备吃包子啦! 22 包子[4]来了,被[B]吃了! 23 做了一个包子,分两半! 24 准备吃包子啦! 25 包子[5]来了,被[A]吃了! 26 准备吃包子啦! 27 包子[5]来了,被[B]吃了! 28 做了一个包子,分两半! 29 准备吃包子啦! 30 包子[6]来了,被[A]吃了! 31 准备吃包子啦! 32 包子[6]来了,被[B]吃了! 33 做了一个包子,分两半! 34 准备吃包子啦! 35 包子[7]来了,被[A]吃了! 36 准备吃包子啦! 37 包子[7]来了,被[B]吃了! 38 做了一个包子,分两半! 39 准备吃包子啦! 40 包子[8]来了,被[A]吃了! 41 准备吃包子啦! 42 包子[8]来了,被[B]吃了! 43 做了一个包子,分两半! 44 准备吃包子啦! 45 包子[9]来了,被[A]吃了! 46 准备吃包子啦! 47 包子[9]来了,被[B]吃了! 48 做了一个包子,分两半! 49 准备吃包子啦! 50 包子[10]来了,被[A]吃了! 51 准备吃包子啦! 52 包子[10]来了,被[B]吃了! 53 54 Process finished with exit code 0
三、迭代器
1、可迭代对象
⑴定义:
可直接作用于for循环的数据类型有以下两类:
a、集合类数据类型:list、tuple、dict、set、str
b、genator生成器
这些可直接作用于for循环的对象统称为可迭代对象,即可循环对象Iterable
⑵如何判断一个对象是否是可迭代对象?使用isinstance函数
示例代码如下:
1 from collections.abc import Iterable 2 3 print(isinstance([], Iterable)) 4 print(isinstance({}, Iterable)) 5 print(isinstance('abc', Iterable)) 6 print(isinstance(100, Iterable))
执行结果:
1 D:\oldboy_py\venv\Scripts\python.exe D:/oldboy_py/day3-20230524迭代器与生成器/迭代器.py 2 True 3 True 4 True 5 False 6 7 Process finished with exit code 0
2、迭代器
⑴定义:可以被next()函数调用,可以使用__next__()方法并不断返回下一个值的对象称为迭代器
⑵如何判断一个对象是否是迭代器?同样使用isinstance()函数
示例代码如下:
1 from collections.abc import Iterator 2 3 print(isinstance((x for x in range(10)), Iterator)) 4 print(isinstance([], Iterator)) 5 print(isinstance({}, Iterator))
执行结果:
1 D:\oldboy_py\venv\Scripts\python.exe D:/oldboy_py/day3-20230524迭代器与生成器/迭代器.py 2 True 3 False 4 False 5 6 Process finished with exit code 0
生成器一定是迭代器,但是迭代器不一定是生成器,因为迭代器必须拥有__next__()方法,但是列表等可迭代对象没有该方法,那么如何将可迭代对象转换为迭代器呢?使用iter()函数
示例代码如下:
1 from collections.abc import Iterator 2 3 print(isinstance(iter([]), Iterator))
执行结果:
1 D:\oldboy_py\venv\Scripts\python.exe D:/oldboy_py/day3-20230524迭代器与生成器/迭代器.py 2 True 3 4 Process finished with exit code 0
⑶为什么list、dict、set等不能是迭代器呢?
因为迭代器的计算是惰性的,即需要返回下一个数据时,它才会计算,并且没有明确的开始和结束,而list等必须有明确的开始和结束,因此这种存在无限大可能得数据流才能是迭代器,range()函数其实就是迭代器
⑷如何查看一个对象可以使用的所有方法?使用dir()函数
示例代码如下:
1 >>> dir([]) 2 ['__add__', '__class__', '__class_getitem__', '__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'] 3 >>>
发现没有__next__()方法,因此不是迭代器