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

  

 

posted @ 2018-03-25 17:52  Reid21  阅读(133)  评论(0)    收藏  举报