Python攻克之路-生成器
生成器
通过列表生成式,可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。如果要创建一个包含100万个元素的列表,不仅占用很大的存储空间,或者仅仅需要访问前面几个元素,加载入内存的其他元素就充分浪费内存空间.
所以,如果列表元素可以按照某种算法推算出来,那是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的内存空间。在Python中,这种一边循环一边计算的机制(通俗说就是需要一个值取一个值,而且只会占用一个元素的内存空间),称为生成器(generator)
1. 列表生成式****
描述:生成一个列表的表达式,一般列表[1,2,3],如下是把for x in range(10)内容作遍历放到前面的x
注意:第一个元素和for后的元素要保持一致,如前面是x后面也要是x
In [1]: [x for x in range(10)] Out[1]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] In [3]: [x*2 for x in range(10)] ##可以对列表做操作,生成新的列表 Out[3]: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
生成式中存放函数
先定义一个函数 In [5]: def f(n): ...: return n**3 for循环调用 In [6]: [f(x) for x in range(10)] Out[6]: [0, 1, 8, 27, 64, 125, 216, 343, 512, 729] In [7]: a=[f(x) for x in range(10)] In [8]: type(a) Out[8]: list
两个元素赋值(注:赋值的元素数量要两边相等)
In [9]: t=('393',8)
In [10]: a,b=t
In [11]: a
Out[11]: '393'
In [12]: b
Out[12]: 8
2. 生成器****
描述:通过如下方式只能取得一个对象,如果是列表表达式,有10个值,相当于是10盘已经作好的菜,什么时候要吃就吃,列表也是可以这样操作,但是存在一个问题它占用内存空间,生成器就相当于它是一个厨师,什么时候要吃一道菜时,可以让它做出来,它只需要调用一个方法,不吃就不会占用空间,所以当列表数量特别大时,列表相当占用内存空间
生成器的两种创建方式
创建方式a. s=(x for x in range(10))
In [13]: s=(f(x) for x in range(10)) #修改封闭方式,把中括号修改成小括号 In [14]: print(s) <generator object <genexpr> at 0x7f7ddef4b0a0> ##生成器对象 In [15]: [f(x) for x in range(100000000000000000000)] Killed
使用生成器的next方法来调用值(next方法只能按顺序来取值)
In [3]: s=(x for x in range(10)) In [4]: s.__next__() ##在python2上调用没有下划线,但是在python3是内部特殊方法的调用,不建议这样使用 Out[4]: 0 In [5]: s.__next__() Out[5]: 1 In [6]: next(s) ##python3上建议使用的内置方法,超出range(10)的范围会报错,因为迭代结束 Out[6]: 2 In [7]: next(s) Out[7]: 3
生成器本向是一个可迭代对象,所以可以使用for循环
描述:a.s是一个数,遍历时会把第一个数取出,实际是for对s在内部进行一个next的调用来取值,第一次把next的值赋值给i,就可以打印出来了0,再循环再回来取得1,然后之前的0就会作为垃圾被回收,即使打印100万个数也只是占用一个数的内存空间,因为没有被引用的会被python的解释器所回收
b.for反复的调用next,但是按照常理,到最后一个数时代表迭代结束,应该会报错误StopIteration,这时for循环通过异常检测出来,也就是通过except的关键字来捕错误,一旦发生错误except就可以取得,来判断是否迭代结束,然后就直接返回不再做任何的处理

In [8]: for i in s: ...: print(i) ...: 4 5 6 7 8 9
b. yield创建方式
一般函数
In [4]: def f(): ...: return 1 ...: f() ...: Out[4]: 1
yield:(主要用于协程)
执行顺序

In [12]: def f():
...: print('ok')
...: yield 1 ##类型于return的功能,把1返回,如果下面再有代码不会执行,除非再加yield
...: print('ok')
...: yield 2
...: ##在yield 2后面有一个默认的return none,代表函数结束
In [13]: a=f() ##f()是一个生成器对象,赋值给a,这个结束后不会执行f,不会执行f
In [14]: print(a)
<generator object f at 0x7f013e847a40> ##是一个生成器对象,只要有yield就是生成器对象
In [15]: next(a)
ok
Out[15]: 1 #return 1
In [16]: next(a)
ok
Out[16]: 2 #return 2
for循环调用原理
描述:调用f()生成器对象,调用next,第一次调用next时,print('ok'),然后返回1,再把1赋值给i,再next,从yield1开始走,print('ok'),返回2,再赋值给i,print(2),第三次再进去函数发现没有yield在会
发生迭代错误,for捕捉异常,迭代结束
注:for i in 后面加的是可迭代对象,for i in后面可以加一个列表[1,2,3],但是列表不是迭代器,因为假设a=[1,2,3],a.时就没有next方法,可迭代对象是内部有__iter__()方法的才是,列表、元组、字典
都有inter方法
In [17]: for i in f():
...: print(i)
...:
ok
1
ok
2
#for内部实际运行过程
In [18]: for i in f():
...: while True:
...: i=next(f())
c.生成器的第二个方法send
描述:send与next功能相似,而且还可以向函数传入值,也就是可以给yield前面的变量传入值
In [48]: def boo():
...: print('ok1')
...: yield 1
...: print('ok2')
...: yield 2
In [49]: b=boo()
In [50]: next(b)
ok1
Out[50]: 1
In [51]: b.send('bbb') ##没报错
ok2
Out[51]: 2
第一次send时只能使用none,因为它不知道赋值给那个变量,如果第一次send前有next,即使不知道传入给那个变量也不会报错
流程:b.send(None)与next(b)一样,进入函数体,打印ok1,到count=yield1,这里yield 1是直接返回1,第2次b.send('bbb'),这里传入bbb值会赋值给count,再执行打印count,yield 2直接返回2
作用:有时需要与程序交互,需要在调用它时给它一些参数,再用send传入给它一个指导作用

In [52]: def boo():
...: print('ok1')
...: count=yield 1
...: print(count)
...: yield 2
In [53]: b=boo()
In [54]: b.send(None) ##相当于next(b)
ok1
Out[54]: 1 ##已经返回1
In [55]: b.send('bbb')
bbb
Out[55]: 2
3.通过生成器yield实现伪并发
描述:很久前CPU只有颗,在同一时刻,CPU只能执行一个任务,但是却做到"伪并"发的效果,如在电脑上既看电影,同时又在QQ聊天,实际是在两个程序之间不段的切换来执行,只是在切换过程中速度快到人所无法感知,如下不太贴切吃苹果就是模拟这种伪并发,一个产苹果,A,B同时吃的简单例子
In [5]: import time
In [6]: def consumer(name): ##吃苹果的人
...: print("%s Prepare apple!" %name) ##传入参数A是人名,准备苹果,接着是B
...: while True:
...: apple = yield ##yield状态停住,下次有参数传入,apple可以接收
...: print("apple[%s]is coming,[%s]eat!"%(apple,name))
...:
In [7]: def producer(name): ##生产苹果的人,一产一吃达到这种并发的效果
...: c = consumer('A') ##创建两个变量,执行consumer函数分别传入参数A,B
...: c2 = consumer('B') ##这两行生成一个生成器对象
...: c.__next__() ##通过next执行,返回,A准备吃苹果
...: c2.__next__() ##通过next执行,返回,B准备吃苹果
...: print("I am ready to eat!")
...: for i in range(2):
...: time.sleep(1)
...: print("prepare two apples!") ##就可以准备,模拟停止了1秒钟,准备两个苹果
...: c.send(i)
##发送i给c生成器对象,c带i进入到consumer函数apple=yield中,i就给apple这个变量,for i in range(2),
从0开始,apple=0,就打印apple[0]is coming,[A]eat!,然后while循环继续又回到yield,c.send(i)就执行
完成,再到c2.send(i)执行
...: c2.send(i)
In [8]: producer("reid")
A Prepare apple!
B Prepare apple!
I am ready to eat!
prepare two apples!
apple[0]is coming,[A]eat!
apple[0]is coming,[B]eat!
prepare two apples!
apple[1]is coming,[A]eat!
apple[1]is coming,[B]eat!
4.斐波那契数列
# 0 1 1 2 3 5 8 13 21
In [18]: def fibo(max):
...: n,before,after=0,0,1 ##n是第几位,before前一位数,after是后一位数
...: while n < max:
...: print(after) ##从1开始打印
...: before,after=after,before+after
#第2位1的值已经打印出来,要做相应的计算,一开始before是0,after是1,移位后before是1,after是before+after=0+1
...: n = n + 1 ##让n+1进行下一次打印,第二次打印after,从1还是变成1
In [32]: fibo(6)
1
1
2
3
5
8
In [33]: def fibo(max):
...: n, before, after=0, 0, 1
...: while n < max:
...: print(before)
...: before,after=after,before+after
...: n = n + 1
...:
In [34]: fibo(6) ##打印before从0开始
0
1
1
2
3
5
补充:before,after=after,before+after运行原理
before=1
after=2
before,after=after,before+after
描述:先从右边开始运行before,after=(after=2),(before+after=1+2=3),再给左边赋值
使用yield来返回,使用next来调用斐波那契数列
In [35]: def fibo(max):
...: n,before,after=0,1,1
...: while n<max:
...: yield before #yield有断层,第一次执行完后,把这个状态保存了,下次再从这里开始
...: before,after=after,before+after
...:
In [37]: fibo(6)
Out[37]: <generator object fibo at 0x7f013d4a7990> #生成器对象,传入的6并不在内存地址,在调用时才会取,而且有限制
In [38]: a=fibo(5)
In [39]: next(a)
Out[39]: 1
In [40]: next(a)
Out[40]: 1
In [41]: next(a)
Out[41]: 2

浙公网安备 33010602011771号