所谓生成器

关于生成器,我们可以这样理解:带有 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的前三个值依次是:1、1、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
posted on 2019-04-19 20:04  江湖乄夜雨  阅读(221)  评论(0编辑  收藏  举报