想带你搞懂python生成器

1. 什么是生成器? 介绍生成器之前,我们可以回忆一下,python中函数的调用方式。普通函数调用,函数会立即执行直到函数出现return关键字或者执行到最后一行。
明明是生成器,为什么要提到函数呢?这是因为大多数时候生成器是以函数来实现的。

  • 普通函数:返回一个值给调用者,把值返回给调用者以后,这个函数就死掉了,也就是被销毁了。
  • 生成器函数:yield(“生出”) 一个值给调用者,yield(“生出”)了一个值以后,函数还活着,调用者有需要的时候会接着生第二个值、第三个值、第四个值。。。
看不懂?没关系,我们先看看下面一个例子。
编程源于生活:神奇的包子铺
楼下王大爷开了一件包子铺,你可不要小瞧这件包子铺,这件包子铺有两个神奇的蒸笼,只要把蒸笼放在蒸架上就能自己产生包子。
小A跟小B同时去吃包子。小B点了50个包子,王大爷就使用神奇的蒸笼一下子给了小B蒸了50个,并且这50个包子使用了50个小碗来装,装完以后,王大爷就把蒸架撤下了,于是小B开始坐下吃包子。
小A也买了50个包子,但是他跟王大爷说,你把我的包子放在蒸笼里面,我每次只吃一个。于是王大爷给了小A一个小碗,小A每吃完一个包子,就去蒸笼里面拿一个包子,蒸笼被小A打开的时候,就产生了一个包子给他。
在这里面:
  • 小A:生成器函数调用者
  • 小B:普通函数的调用者
  • 小A的蒸笼:生成器函数(小A拿了一个包子以后,继续放在蒸架上准备剩下的49个包子,函数保留状态,可以记录已经拿了几个,还剩下几个)
  • 小B的蒸笼:普通函数(给小B拿了50包子,直接就被王大爷收起来了,函数被销毁,还想吃包子就需要王大爷把蒸笼重新放上蒸架)
  • 小A用1个小碗吃:占用内存大小(1KB)
  • 小B用50个小碗吃:占用内存大小(50KB)
  • 王大爷:CPU
def simple_generator(): x=1 yieldgenrator = simple_generator()# 函数内使用yield关键字,会返回一个生成器对象 print(type(genrator))#

老样子,看看生成器对象里面有什么干货
print(dir(genrator)) [..., '__iter__', '__next__'...]# 又看到了我们的老朋友。。。迭代器里面的两个兄弟

这里我们能得到什么结论呢?
生成器也是一个迭代器,它具备迭代器的功能。生成器是一个特殊的迭代器
不熟悉迭代器的朋友可以看看我上一期的文章。
2. 创造生成器 2.1 通过yield关键字
def simple_generator(): x=1 yield x# 第一次调用next(),执行到这里就停下,返回xgenrator = simple_generator()print(genrator) # print(type(genrator)) #

如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个生成器(generator)
2.2 生成器表达式
generator = (i for i in range(10)) print(generator)

列表推导式的 [ ] 改成 ( )就可以创建一个生成器
那生成器跟列表有什么不同的呢?来举一个非常直观的例子
_list = [i for i in range(10)]print("取出第一个包子:",_list[0])# 取出第一个包子:0 print("取出第二个包子:",_list[1])# 取出第二个包子:1 print("取出第三个包子:",_list[2])# 取出第三个包子:2for i in _list: print("取出包子序号:",i)取出包子序号: 0 取出包子序号: 1 取出包子序号: 2 取出包子序号: 3 取出包子序号: 4 取出包子序号: 5 取出包子序号: 6 取出包子序号: 7 取出包子序号: 8 取出包子序号: 9

generator = (i for i in range(10))print("取出第一个包子:",next(generator))# 取出第一个包子:0 print("取出第二个包子:",next(generator))# 取出第二个包子:1 print("取出第三个包子:",next(generator))# 取出第三个包子:2for i in generator: print("取出包子序号:",i)取出包子序号: 3# 这里跟列表有点不一样,列表每次都从0开始,而生成器只能从当前已经拿到的数开始 取出包子序号: 4 取出包子序号: 5 取出包子序号: 6 取出包子序号: 7 取出包子序号: 8 取出包子序号: 9

对比上面的包子铺:
小B一次性拿到了50个包子,每个包子放在一个碗里面,假设给碗编号,那么小B可以通过编号任意拿一个包子,小B可以随意给包子排列组合,小B还喜欢数包子,小B就一直数自己有多少个包子,反反复复数都可以。
但是小A的情况就不一样了。他只有一个碗,一次只能装一个,拿了1号包子以后,要想拿2号包子,就只能把1号包子丢掉或者吃掉。小A还不能数包子,他只能记录自己已经拿了几个包子
  • 小B通过编号任意拿包子:列表索引取值
  • 小A拿完1号包子再拿二号:通过next()函数取值
  • 小A拿完2号就再也不能拿一号(一号已经被丢掉/吃掉):生成器只能执行一次
  • 小B数包子可以重复多次,并且每次都能从1号开始数:for ... in ... 可以多次,每次都可以从索引为0开始
  • 小A只能从当前拿到的包子开始数,一旦数完就再也没法数:for ... in ... 只能一次,当前拿到第几个数,就从这个数开始遍历
结论:生成器保存的是算法,每次调用next(),就计算出下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误。
3. yield关键字 yield这个关键字是一个比较抽象的概念。
还是包子铺:
王大爷把蒸笼放在蒸架上开始蒸包子,小A每次打开蒸笼盖子,蒸笼会当场捏一个蒸熟了的包子给他,并且自动关上蒸笼盖。
  • 王大爷:CPU
  • 蒸笼:生成器函数
  • 蒸架:内存空间
  • 蒸笼上架:加载函数
  • 打开蒸笼盖子:执行next()方法
  • 将包子给小A:yield(生成)了一个值给调用者
  • 关上蒸笼盖:函数退出(也可以理解为暂停)
  • 下一次打开盖子:又执行next()方法,从上一次yield的地方开始执行,遇到下一个yield又退出
def make_baozi(xx): return xxdef simple_generator(): print("第一次制作猪肉白菜馅的包子") formulation = "猪肉、白菜" x_zhu = make_baozi(formulation) yield x_zhu # 第一次开盖子,做好猪肉白菜包子返回给你。暂停,等你吃完,并且记录我已经把猪肉白菜包子给你了 # 下一次执行上面这块将不会再执行了,而是从这个关键字往后开始执行print("第二次制作叉烧馅的包子") formulation = "叉烧" x_cha = make_baozi(formulation) yield x_cha# 第二次开盖子,做好叉烧包子返回给你,暂停,等你吃完,并且记录我已经把猪肉白菜包子、叉烧包子给你了print("第三次制作玉米馅的包子") formulation = "玉米" x_yu = make_baozi(formulation) yield x_yu# 第一次开盖子,做好玉米包子返回给你,暂停,等你吃完,并且记录我已经把猪肉白菜包子、叉烧包子、玉米包子给你了genrator = simple_generator()print(next(genrator)) # 第一次拿猪肉白菜馅的包子 # 猪肉、白菜print(next(genrator)) # 第二次拿叉烧馅的包子 # 叉烧print(next(genrator)) # 第三次拿玉米馅的包子 # 玉米print(next(genrator)) Traceback (most recent call last): File "/app/util-python/test.py", line 36, in print(next(genrator)) StopIteration

我们可以把yield理解成为函数的暂停键,next()函数是开始键。
暂停的同时,也会将值返回给你。等下一次开始的时候,就从上一次暂停的地方继续执行。
3.1 yield from Python3.3版本的PEP 380中添加了yield from语法。yield from 可以直接把可迭代对象中的每一个数据作为生成器的结果进行返回
def simple_generator(): a = [1,2,3] yield agenrator = simple_generator() print(genrator.__next__()) # [1,2.3]

def simple_generator(): a = [1,2,3] yield from agenrator = simple_generator() print(genrator.__next__())# 1 print(genrator.__next__())# 2 print(genrator.__next__())# 3

4. 生成器方法 生成器是迭代器的一种,生成器比迭代器多了三种方法:send()close()throw()
4.1 send 当生成器处于暂停状态时,向生成器传一个值
def simple_generator(): a = "测试" a = yield a yield agenrator = simple_generator() print(genrator.send("dd"))

Traceback (most recent call last): File "/app/util-python/test.py", line 12, in genrator.send("dd") TypeError: can't send non-None value to a just-started generator

上面的用法报错了,为什么呢?因为此时我们的生成器还没有启动,我们需要先启动生成器。
启动生成器的方法1:
print(genrator.send(None))

启动生成器的方法2:
print(genrator.__next__())

生成器启动后,再尝试一下:
def simple_generator(): a = "测试"# 第一次启动会执行到这里,暂停后,可以通过send向这个关键字这里传参 a = yield a print("下次执行的代码块") yield agenrator = simple_generator() print(genrator.__next__())# 打印:测试 print(genrator.send("new value"))# 打印:new value

总结一下:这个方法可以向生成器发送一个参数,但是生成器必须先启动,也就是必须先执行到第一个 yield 关键字的地方,然后暂停在这个关键字这。此时按下暂停键的这个 yield 就可以接受外部send的值的
4.2 throw() 在生成器函数执行暂停处,抛出一个指定的异常
def simple_generator(): a = "开始执行" try: yield a except ValueError: print("捕获到了抛进来的异常")b = "执行第二个yield" yield bgenrator = simple_generator() print(genrator.__next__()) # 执行到 yield a 处,所以这里应该是打印:开始执行print(genrator.throw(ValueError)) # 从 yield a 处往下开始执行,抛出一个 ValueError 异常,如果抛出的异常被处理掉,那么就会接着往下执行到yield b 处,否则直接抛出异常,程序停止 # 所以此处的结果应该打印:执行第二个yield

可以跟下面的代码对比这着看看,应该能加深理解
def simple_generator(): a = "开始执行" try: yield a raise ValueError except ValueError: print("捕获到了抛进来的异常")b = "即将准备执行第二个yield" yield bgenrator = simple_generator() print(genrator.__next__()) # 执行到 yield a 处,所以这里应该是打印:开始执行print(genrator.__next__()) # 从 yield a 处往下开始执行,执行 raise ValueError抛出一个 ValueError 异常,紧接着执行到 yield b 处

4.3 close() 向生成器抛出一个GeneratorExit异常,意味着生成器生命周期结束。
def simple_generator(): a = "开始执行"try: yield a except ValueError: print("捕获到了抛进来的异常")except GeneratorExit: print("生成器退出")b = "执行第二个yield" yield bgenrator = simple_generator() print(genrator.__next__()) genrator.close()

上面这段代码最终结果:
Traceback (most recent call last): File "/app/util-python/test.py", line 23, in genrator.close() RuntimeError: generator ignored GeneratorExit

因为生成器已经执行了genrator.close()方法,抛出了了GeneratorExit异常,生成器方法后续执行的语句中,不能再有yield语句,否则会产生 RuntimeError
【想带你搞懂python生成器】所以这里需要下面两行代码去掉。或者先执行一次 genrator.__next__()再执行genrator.close(),让函数将所有的 yield 执行完再 close
b = "执行第二个yield" yield b

5. 实现斐波拉契数列(Fibonacci)
def fib(max): n = 0 a = 0 b = 1 while n < max: yield b a, b = b, a+b n+=1r = fib(10) for i in r: print(i) 1 1 2 3 5 8 13 21 34 55

    推荐阅读