生成器(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实现协程——生产者消费者模型
所谓协程,可以简单理解为函数之间相互“切换”,