关于生成器,咱们能够这样理解:带有 yield 的函数在 Python中被称之为 generator(生成器)。
生成器有关的说明以下:
(1)一个带有 yield 的函数就是一个 generator,它和普通函数不一样,生成一个 generator 看起来像函数调用,但不会执行任何函数代码,直到对其调用 next()(在 for 循环中会自动调用 next())才开始执行。
(2)虽然执行流程仍按函数的流程执行,但每执行到一个 yield 语句就会中断,并返回一个迭代值,下次执行时从 yield 的下一个语句继续执行。
(3)看起来就好像一个函数在正常执行的过程当中被 yield 中断了数次,每次中断都会经过 yield 返回当前的迭代值。
(4)事实上,在咱们建立了一个generator后,基本上永远不会调用next(),而是经过for循环来迭代,而且不须要关心StopIteration的错误。
获得一个生成器
获得生成器的方式有两种:一种是生成器表达式
另一种是函数的方法
生成器表达式
其实,生成器表达式就是将列表推导式
中的[]
改为()
就能够了:
generator = (i**2 for i in range(1,6))
函数的方法
利用函数的方法获取一个生成器,这个函数中一定包含yield
关键字:
def generator(max):
while max > 0:
yield max
max -= 1
获取生成器中的值
咱们能够经过next()方法(或者__next__())以及for循环迭代获取生成器中的值
next方法获取生成器的值:
generator = (i for i in range(1,6))
print(next(generator))
print(next(generator))
print(next(generator))
print(next(generator))
print(next(generator))
# 生成器中只有5个数,超出范围会抛出StopIteration异常
print(next(generator))
很明显,咱们的生成器中只有5个值,可是取值的时候却next了6次,因此在第六次视图取值的时候,程序会抛出StopIteration异常
。
for循环迭代
而利用for循环的方法不用关心StopIteration异常:
generator = (i for i in range(1,6))
for num in generator:
print(num,end=' ')
结果为:
1 2 3 4 5
为何要用生成器
生成器存在的最大的意义就是:节省内存空间
咱们来看一个Fibonacci数列的例子:一种占用内存的作法是这样的:
def fib(max):
lst = []
n,a,b = 0,0,1
while n < max:
lst.append(b)
a,b = b,a+b
n += 1
return lst
f = fib(6)
print(f) # [1, 1, 2, 3, 5, 8]
也就是说,咱们把每一次的结果都append到一个列表中去了,最终将这个包含全部数据的列表返回。没错!聪明的你或许一眼就看出问题来了:若是这个max设置的特别大,那岂不意味着这个存放着全部数据的lst也会跟着增大,结果就会致使内存吃紧!
没错,生成器的存在就是为了解决上面“大量数据占用内存”的问题。
生成器解决上面问题的方法以下:
def fib(max):
n,a,b = 0,0,1
while n < max:
yield b
a,b = b,a+b
n += 1
f = fib(6)
for i in f:
print(i,end=' ') #1 1 2 3 5 8
对于生成器来讲,它不会将函数产生的数据一次性的拿出来,而是在程序须要的时候,将数据一个一个的生产
出来,相比于前面用列表一次性的将数据取出的方法,大大节省了程序对内存的占用,而这也是生成器在实际中最经常使用的情景之一。
生成器的执行流程
对于生成器的执行流程,咱们用下面代码来讲明下:
def fib():
a = b =1
yield a
yield b
while 1:
a , b = b , a+b
yield b
g = fib()
for num in fib():
if num > 10:break
print(num)
这段代码实际上是Fibonacci数列的另外一种实现方式。首先,咱们定义了一个生成器函数fib,而后将这个函数的执行结果赋值给g,也就是说,这里的g就成为了一个生成器。
当for循环开始遍历(迭代)这个生成器的时候执行fib函数内部的代码:第一句是将1赋值给a和b,接着遇到了yield a
语句,当程序遇到yield语句时会暂时停下来
,不执行后面的代码,而此时,咱们在函数的外面就能够经过next(生成器对象)
的方法获取当前yield后面的值
(注意,for循环中自带了next()方法),而咱们在for循环中获得的第一个值就是当前的a的值1
;接着,for循环开始遍历第二个数(至关于执行第二个next(g))的时候,又发现了yield b
,根据前面的说明,此时会打印第二个yield后面的b的当前值 1
;在for循环进行第三次遍历的时候进入while循环:首先将b的当前值赋值给a,而后将a+b的值赋值给b(Fibonacci数列的算法),而后遇到了第三个yield,所以第三次遍历至关于执行了第三次next(g),因而此时会打印当前的b的值2
,因此num的前三个值依次是:一、一、2
。在第四次遍历的时候,再次在while循环中进行数据的赋值与交换操做,直到获得的值num不知足num>10
这个条件为止。
总的来说,其实就是函数在执行的过程当中只要遇到yield
关键字就会中止
,等待外面发出next(生成器对象)的信号再将yield后面的值返回。
下面的例子可能会更有助于你们理解:
咱们先打印一个next(g)看一下结果:
def count(n):
while n > 0:
print('before yield')
yield n
n -= 1
print('after yield')
g = count(5)
print(next(g))
结果为:
before yield
5
而后咱们打印两次next(g)的时候看一下结果:
before yield
5
after yield
before yield
4
咱们能够看到"after yield"实际上是在第二次执行next(g)的时候打印的,这也充分说明了第一次的时候count函数停在了yield n那里。
从上面的例子咱们还能够看到:如一个函数中出现多个yield则next()会中止在下一个yield前
def generator():
print('one')
yield 123
print('two')
yield 456
print('end')
g = generator()
# 第一次运行,暂停在 yield 123,打印one与123
print(next(g))
# 第二次运行,暂停在 yield 456,打印two与456
print(next(g))
# 第三次运行,先打印end,可是因为后面没有yield语句了,所以再使用next()方法会报错
print(next(g))
上面代码的结论须要好好理解。
生成器中的return
关于生成器中用return,我的总结有逻辑结束
与显示调用
两种
逻辑结束
所谓逻辑结束,其实就是咱们在设计程序的时候,在不知足一些条件的状况下,直接使用return跳出函数:
def read_file(path):
size = 1024
with open(path,'r') as f:
while True:
block = f.read(size)
if block:
yield block
else:
return
这种状况下使用return其实是从程序的安全性考虑的,当咱们读取一个文件的时候若是遇到空文件直接跳出函数,避免了read获得的无效数据导致后续操做抛出异常。
显示调用
从网上查看相关文档,有这样的说法:做为生成器,由于每次迭代就会返回一个值,因此不能显示的在生成器函数中return 某个值,包括None值也不行,不然会抛出“SyntaxError”的异常
。可是本人在测试的时候发现这种状况只在python2中会有,我本身用的python3.6.8解释器并无报错:
python2解释器下的状况:
Python 2.7.15 (v2.7.15:ca079a3ea3, Apr 30 2018, 16:30:26) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> def func():
... yield 123
... return 666
...
File "<stdin>", line 3
SyntaxError: 'return' with argument inside generator
python3.6.8解释器运行结果:
Python 3.6.8 (tags/v3.6.8:3c6b436a57, Dec 23 2018, 23:31:17) [MSC v.1916 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> def func():
... yield 123
... return 666
...
>>> g = func()
>>> print(next(g))
123
yield返回值与send方法
看下面代码:
# -*- coding:utf-8 -*-
def func():
print('one')
yield 123
print('two')
yield 456
print('end')
g = func()
print(next(g))
print(next(g))
结果为:
one
123
two
456
这里你可能会想:yield 123咱们打印出来123,yield 456打印出了456,那么,123与456是否是yield的返回值呢?答案固然不是。
实际上,咱们获取到的yield后面的值实际上是经过next()方法获得的,而yield自己是有返回值的,默认状况是None
咱们在上面的代码基础上作一些改动来看一下:
def func():
print('one')
a = yield 123
print(a)
print('two')
yield 456
print('end')
g = func()
print(next(g))
这里咱们将第一个yield的返回值赋值给了a,接下来打印a。可是因为程序运行到第一个yield的时候会停下来,不会接着执行,所以第一次不会打印a,结果为:
one
123
而若是咱们在上面代码的基础上再加一个next(g)
的话,打印的结果以下:
one
123
None
two
456
咱们会发如今进行到第二个yield的时候123与two之间打印出了None,这个None其实就是第一个yield的默认返回值
想要修改这个返回值,或者说为其赋值的话,咱们就可使用send方法
:经过send方法去为上一次被挂起的yield语句赋值。
看下面的代码:
def my_generator():
value = yield 1
value = yield(value)
value = yield(value)
g = my_generator()
print(next(g))
print(g.send('hello'))
print(g.send('world'))
结果为:
1
hello
world
具体的过程说明以下:
(1)当调用gen.next()方法时,python首先会执行MyGenerator方法的yield 1语句。因为是一个yield语句,所以方法的执行过程被挂起,而next方法返回值为yield关键字后面表达式的值,即为1。
(2)当调用gen.send('hello')方法时,python首先恢复MyGenerator方法的运行环境。同时,将表达式(yield 1)的返回值定义为send方法参数的值,即为'hello'。
这样,接下来value=(yield 1)这一赋值语句会将value的值置为'hello'。继续运行会遇到yield value语句。所以,MyGenerator方法再次被挂起。
同时,send方法的返回值为yield关键字后面表达式的值,也即value的值,为'hello'。
(3)当调用send('world')方法时MyGenerator方法的运行环境。同时,将表达式(yield value)的返回值定义为send方法参数的值,即为'world'。
这样,接下来value=(yield value)这一赋值语句会将value的值置为'world'。第三次打印'world'。
能够看到:能够看出来:第一个的next取到了1;咱们把'hello'赋值给第一个yield做为其返回值,因此第二次取到的是'hello',一样的,第三次取到的是咱们为第二个yield表达式send的返回值'world'。
总的来讲,send方法和next方法惟一的区别是在执行send方法会首先把上一次挂起的yield语句的返回值经过参数设定,从而实现与生成器方法的交互。
可是须要注意,在一个生成器对象没有执行next方法以前,因为没有yield语句被挂起,若是非要是用send方法,那么这个在第一个位置的send方法里面的参数必须是None,不然会报错。
下面是错误的写法:
def my_generator():
value = yield 1
value = yield(value)
value = yield(value)
g = my_generator()
print(g.send('hello'))
print(g.send('world'))
程序会报这样的错:
TypeError: can't send non-None value to a just-started generator
若是非要在第一次使用send方法,正确的写法是在send方法中加参数None:
def my_generator():
value = yield 1
value = yield(value)
value = yield(value)
g = my_generator()
print(g.send(None))
print(g.send('hello'))
print(g.send('world'))
结果为:z
1
hello
world
由于当send方法的参数为None时,它与next方法彻底等价。可是注意,虽然上面的代码能够接受,可是不规范。因此,在调用send方法以前,仍是先调用一次next方法为好。
利用yield实现简单的协程案例——生产者消费者
这是yield十分关键的用处,理解了yield的机制对理解协程并进行相关并发的程序设计十分有帮助!
所谓协程
,能够简单理解为函数之间的相互切换
。而利用yield与send方法咱们能够十分方便的实现这种效果:
# -*- coding:utf-8 -*-
import time
def consumer():
# consumer做为一个生成器
while 1:
data = yield
def producer():
# 生成器对象
g = consumer()
# 先next后面才能send具体的非None的值,至关于先send一个None
next(g)
for i in range(1000000):
g.send(i)
if __name__ == '__main__':
start = time.time()
#并发执行,可是任务producer遇到io就会阻塞住,并不会切到该线程内的其余任务去执行
producer()
print('执行时间:',time.time() - start)
结果为:
执行时间: 0.12068915367126465
固然这涉及到了协程与IO阻塞相关的知识,咱们这里不作讨论,上述函数是为了说明yield与send在函数任务之间不断切换的功能
参考文献:
https://www.cnblogs.com/wj-1314/p/8490822.html
https://blog.csdn.net/jason_cuijiahui/article/details/84947310
https://blog.csdn.net/zxpyld3x/article/details/79181834
https://blog.csdn.net/hedan2013/article/details/56293173