想带你搞懂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
对比上面的包子铺:结论:生成器保存的是算法,每次调用next(),就计算出下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误。
小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 ...
只能一次,当前拿到第几个数,就从这个数开始遍历
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 执行完再 closeb = "执行第二个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
推荐阅读
- 我喜欢做菜,是因为懂得了什么是爱
- 一个懂得和他自己灵魂沟通的人,这个人一定是正直善良的人
- 不懂法,害人终害己
- 只有经历了才真正懂得的伤痛
- 【图解】9张图彻底搞懂堆排序
- 我一点都不想做懂事的姑娘
- 为什么懂得那么多道理却过不好这一生()
- 回到最初懵懂
- 渐行
- 相见不如怀念,那个至死不见刘彻的李夫人,最懂男人心!