生成器(generator)中 的yield 以及几个实例

  带有 yield 的函数在 Python 中被称之为 generator(生成器)

先记住以下结论:

(1)一个带有 yield 的函数就是一个 generator,它和普通函数不同,生成一个 generator 看起来像函数调用,但不会执行任何函数代码,
   直到对其调用 next()(在 for 循环中会自动调用 next())才开始执行。

(2)虽然执行流程仍按函数的流程执行,但每执行到一个 yield 语句就会中断,并返回一个迭代值,下次执行时从 yield 的下一个语句继续执行。

(3)看起来就好像一个函数在正常执行的过程中被 yield 中断了数次,每次中断都会通过 yield 返回当前的迭代值。

  yield 的好处是显而易见的,把一个函数改写为一个 generator 就获得了迭代能力,比起用类的实例保存状态来计算下一个 next() 的值,不仅代码简洁,而且执行流程异常清晰。

一:实例1:Fibonacci数列

下面是一个使用yield实现Fibonacci数列的例子:(如果使用列表来存储的话,如果数据大了会很占用内存)

# -*- coding:utf-8 -*-
def fib(max):
    n,a,b = 0,0,1
    while n < max:
       yield b
       a,b = b,a+b
       n += 1

f = fib(6)
print(next(f))
print(next(f))
print(next(f))
print(next(f))
print(next(f))
print(next(f))

  但是,如果next的数量超过了限制的话,generator 自动抛出 StopIteration 异常,表示迭代完成。而在 for 循环里,无需处理 StopIteration 异常,循环会正常结束

# -*- coding:utf-8 -*-
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)

  上面这样利用for循环写不会报错。

**********************第二种写法:**********************

# -*- coding:utf-8 -*-
def fib():
    a = b =1
    yield a
    yield b
    while 1:
        a , b = b , a+b
        yield b

g = fib()
# 取10个数
for i in range(10):
    print(next(g))   
'''
# 也可以这样写:
for num in fib():
    if num > 10:break
    print(num)
''' 

实例二:用yield生成器模拟Linux中命令:tail -f file | grep python 用于查找监控日志文件中出现有python字样的行

# 注意程序只检测新增的日志信息!
#当程序运行时,若warn.log文件中末尾有新增一行,且该一行包含python,该行就会被打印出来
#若打开warn.log时,末尾已经有了一行包含python,该行不会被打印,因为上面是f.seek(0,2)移动到了文件EOF处
#故,上面程序实现了tail -f warn.log | grep 'python'的功能,动态实时检测warn.log中是否新增现了
#新的行,且该行包含python

def tail(f):
    # 移动到文件的EOF最后
    f.seek(0.2)
    while 1:
        # 读取文件中新的文本行
        line = f.readline()
        if not line:continue
        # yield 出每一行的数据
        yield line

def grep(lines,search_text):
    for line in lines:
        if search_text in line:
            yield line

if __name__ == '__main__':
    flog = tail(open('log.log'))
    py_lines = grep(flog,'python')
    for line in py_lines:
        print(line)

实例三:有助于理解的例子

# -*- coding:utf-8 -*-
def count(n):
    while n > 0:
        print('before yield')
        yield n
        n -= 1
        print('after yield')

g = count(5)

print(g.send(None))

print(next(g))
# print(next(g))
#
# print(g.__next__())
# print(g.__next__())

结果:

实例四:yield中return的作用

 

作为生成器,因为每次迭代就会返回一个值,所以不能显示的在生成器函数中return 某个值,包括None值也不行,否则会抛出“SyntaxError”的异常,但是在函数中可以出现单独的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

实例五:如一个函数中出现多个yield则next()会停止在下一个yield前

# -*- coding:utf-8 -*-
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))

实例六:关于yield的返回值与send方法

   实际上,yield后面的数是我们通过next()方法取到的,也就是说123与456的结果是我们通过打印next(g)的方法获取的!

  而yield实际上也是有返回值的,看下面代码:

 

# -*- coding:utf-8 -*-
def generator():
    print('one')
    a = yield 123
    print(a)
    print('two')
    yield 456
    print('end')
g
= generator() # 第一次运行,暂停在 yield 123 print(next(g))

 

结果为:

one
123

然后接着加代码:

# -*- coding:utf-8 -*-
def generator():
    print('one')
    a = yield 123
    print(a)
    print('two')
    yield 456
    print('end')


g = generator()
# 第一次运行,暂停在 yield 123
print(next(g))
# 第二次运行,暂停在 yield 456
next(g)

结果如下:

 

one
123
None
two

 

这里没有打印,直接使用的next方法,可以看到,在第二个yield与第一个结果之间多了一个None,其实,这个None就是“上一次被挂起的yield语句的返回值”,默认为None!

 

而我们可以通过send方法去为上一次被挂起的yield语句赋值

看下面的例子:

# -*- coding:utf-8 -*-
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,否则会报错。

错误的写法:

正确的写法:

 

因为当send方法的参数为None时,它与next方法完全等价。但是注意,虽然上面的代码可以接受,但是不规范。

所以,在调用send方法之前,还是先调用一次next方法为好。

实例七:利用yield实现协程——生产者消费者模型

  所谓协程,可以简单理解为函数之间相互“切换”,

 

posted on 2019-04-19 15:50  江湖乄夜雨  阅读(2760)  评论(0编辑  收藏  举报