Python 生成器

生成器也是一种迭代器,但是你只能对其迭代一次。这是因为它们并没有把所有的值存在内存中,而是在运行时生成值。

生成器保存的是算法,每次调用 next(G) ,就计算出 G 的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出 StopIteration 的异常。当然,这种不断调用 next() 实在是太变态了,正确的方法是使用 for 循环,因为生成器也是可迭代对象。所以,我们创建了一个生成器后,基本上永远不会调用 next() ,而是通过 for 循环来迭代它,并且不需要关心 StopIteration 异常。

generator = 函数 + yield

简单说,就是一个函数,里面用到了关键字 yield,就成为了一个生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> g = [x*x for x in range(5)]
>>> g
[0, 1, 4, 9, 16]
>>> g = (x*x for x in range(5))
>>> g
<generator object <genexpr> at 0x7f2a07743fc0>
>>> for i in g:
... print(i)
...
0
1
4
9
16

>>> for i in g:
... print(i)
...
>>>

看起来除了把 [] 换成 () 外没什么不同。但是,你不可以再次使用 for i in g,因为生成器只能被迭代一次:先计算出 0,然后继续计算 1,然后计算 4,一个跟一个的。

yield 关键字

yield 是一个类似 return 的关键字,只是这个函数返回的是个生成器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
>>> def gen():
... print('---1---')
... mlist = range(3)
... for i in mlist:
... print('---2---')
... yield i*i
... print('---3---')
...
>>> mygen = gen()
>>> next(mygen)
---1---
---2---
0
>>> next(mygen)
---3---
---2---
1
>>> next(mygen)
---3---
---2---
4
>>> next(mygen)
---3---
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

你必须要理解:当你调用这个函数的时候,函数内部的代码并不立马执行,这个函数只是返回一个生成器对象,这有点蹊跷不是吗。

那么,函数内的代码什么时候执行呢?当你使用 for 进行迭代的时候.

第一次迭代中你的函数会执行,从开始到达 yield 关键字,然后返回 yield 后的值作为第一次迭代的返回值。

然后,再次执行时从 yield 的下一跳语句开始执行,然后再次遇到 yield 后,再返回那个值,直到没有可以返回的。

如果生成器内部没有定义 yield 关键字,那么这个生成器被认为成空的。这种情况可能因为是循环进行没了,或者是没有满足 if/else 条件。

在上面的例子,我们在循环过程中不断调用 yield ,就会不断中断。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。同样的,把函数改成 generator 后,我们基本上从来不会用 next() 来获取下一个返回值,而是直接使用 for 循环来迭代:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> for i in mygen:
... print(i)
...
---1---
---2---
0
---3---
---2---
1
---3---
---2---
4
---3---

生成器和函数的区别

  • 直接调用生成器,不会执行;
    • 举个栗子:a = f() # 这里 f() 是个生成器
    • 运行上面这句,f() 不会执行,首次执行需要使用 next(a) 或 a.send(None),后面会细讲
  • 每次执行,会暂时中断在 yield 关键字处,而且通过 yield 可以返回一个参数
  • 下次再接着执行,会从上次中断的 yield 处接着执行,并可以通过 send() 传递参数,当然继续中断在下一个 yield 处
  • 如果通过 send() 或 next() 执行 generator,而没有找到下一个 yield,会报错

next() 和 send()

  • return = send(msg)

    • 传递参数 msg 给 当前中断 yield 前面的变量
    • 同时返回下一个 yield 后面的参数给 return
  • return = next(a)

    • 没有传递参数或者说传递参数 None 给当前中断 yield 前面的变量
    • 同时返回下一个 yield 后面的参数给 return
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 生成器
def f():
print('start')
a = yield 1 # 可以返回参数1,并接收传递的参数给a
print(a)
print('middle')
b = yield 2 # 可以返回参数2,并接收传递的参数给b
print(b)
print('next')
c = yield 3 # 可以返回参数3,并接收传递的参数给c
print(c) # 这里貌似永远不会执行,因为总会在上一行的yield处结束


a = f() # 这里不会执行,即没有任何打印信息
# a.next() #这种写法在python3里面会报错
return1 = next(a) # 输出start,中断在yield 1处,返回yield后面的1给return1
# return1 = a.send(None) # 效果同上一条语句
# return1 = a.send('test') # 这里会报错,生成器启动时只能传入 None,因为没有变量接收传入的值, TypeError: can't send non-None value to a just-started generator

# 如果首次执行generator,就传递一个非None的参数,因为第一次执行不是从一般的中断yield处执行起,所以没有yield关键字来接收传参,就会报错
print(return1)
return2 = next(a) # 传入参数为None,即a=None,返回2给return2
print(return2)
return3 = a.send('msg') # 传入参数msg,即b=msg,返回3给return3
print(return3)

运行结果:

1
2
3
4
5
6
7
8
start
1
None
middle
2
msg
next
3

总结

生成器是这样一个函数,它记住上一次返回时在函数体中的位置。对生成器函数的第二次(或第 n 次)调用跳转至该函数中间,而上次调用的所有局部变量都保持不变。

生成器不仅记住了它数据状态,还记住了它在流控制构造(在命令式编程中,这种构造不只是数据值)中的位置。

生成器的特点:

  • 节约内存
  • 迭代到下一次的调用时,所使用的参数都是第一次所保留下的,即是说,在整个所有函数调用的参数都是第一次所调用时保留的,而不是新创建的
hoxis wechat
一个脱离了高级趣味的程序员,关注回复1024有惊喜~
赞赏一杯咖啡
  • 本文作者: hoxis | 微信公众号【不正经程序员】
  • 本文链接: https://hoxis.github.io/python-generator.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!
  • 并保留本声明和上方二维码。感谢您的阅读和支持!
0%