[要点抢先看]
1. 生成器函数的使用
2. 生成器表达式的使用
3. 与列表解析式的对比及对内存的优化
之前我们介绍了列表解析式, 他的优点很多, 比如运行速度快, 编写简单, 但是有一点我们不要忘了, 他是一次性生成整个列表. 如果整个列表非常大, 这对内存也同样会造成很大压力, 想要实现内存的节约, 可以将列表解析式转换为生成器表达式.
[妹子说] 那今天就要说说生成器咯.
对的, 避免一次性生成整个结果列表的本质是在需要的时候才逐次产生结果, 而不是立即产生全部的结果, python 中有两种语言结构可以实现这种思路.
一个是生成器函数. 外表看上去像是一个函数, 但是没有用 return 语句一次性的返回整个结果对象列表, 取而代之的是使用 yield 语句一次返回一个结果. 另一个是生成器表达式. 类似于上一小节的列表解析, 但是方括号换成了圆括号, 他们返回按需产生的一个结果对象, 而不是构建一个结果列表.
这个 "按需" 指的是在迭代的环境中, 每次迭代按需产生一个对象, 因此, 上述二者都不会一次性构建整个列表, 从而节约了内存空间.
[妹子说] 那举几个例子说说吧.
好, 下面具体结合例子说说生成器函数.
首先, 我们还没有详细介绍过函数, 先简单说一下, 常规函数接受输入的参数然后立即送回单个结果, 之后这个函数调用就结束了.
但生成器函数却不同, 他通过 yield 关键字返回一个值后, 还能从其退出的地方继续运行, 因此可以随时间产生一系列的值. 他们自动实现了迭代协议, 并且可以出现在迭代环境中.
运行的过程是这样的: 生成器函数返回一个迭代器, for 循环等迭代环境对这个迭代器不断调用 next 函数, 不断的运行到下一个 yield 语句, 逐一取得每一个返回值, 直到没有 yield 语句可以运行, 最终引发 StopIteration 异常. 看, 这个过程是不是很熟悉.
首先, 下面这个例子证实了生成器函数返回的是一个迭代器
- def gen_squares(num):
- for x in range(num):
- yield x ** 2
- G = gen_squares(5)
- print(G)
- print(iter(G))
- <generator object gen_squares at 0x0000000002402558>
- <generator object gen_squares at 0x0000000002402558>
复制代码
然后再用手动模拟循环的方式来看看生成器函数的运行过程, 你会发现和前面介绍过的熟悉场景并无二致.
- def gen_squares(num):
- for x in range(num):
- yield x ** 2
- G = gen_squares(3)
- print(G)
- print(iter(G))
- print(next(G))
- print(next(G))
- print(next(G))
- print(next(G))
- <generator object gen_squares at 0x00000000021C2558>
- <generator object gen_squares at 0x00000000021C2558>
- 0
- 1
- 4
- Traceback (most recent call last):
- File "E:/12homework/12homework.py", line 10, in <module>
- print(next(G))
- StopIteration
复制代码
那这么看, 在 for 循环等真正的使用场景中使用也不难了
- def gen_squares(num):
- for x in range(num):
- yield x ** 2
- for i in gen_squares(5):
- print(i, end=' ')
- 0 1 4 9 16
复制代码
我们进一步来说说生成器函数里状态保存的话题. 在每次循环的时候, 生成器函数都会在 yield 处产生一个值, 并将其返回给调用者, 即 for 循环. 然后在 yield 处保存内部状态, 并挂起中断退出. 在下一轮迭代调用时, 从 yield 的地方继续执行, 并且沿用上一轮的函数内部变量的状态, 直到内部循环过程结束.
关于这个问题, 具体可以看看这个例子:
- def gen_squares(num):
- for x in range(num):
- yield x ** 2
- print('x={}'.format(x))
- for i in gen_squares(4):
- print('x ** 2={}'.format(i))
- print('--------------')
- x ** 2=0
- --------------
- x=0
- x ** 2=1
- --------------
- x=1
- x ** 2=4
- --------------
- x=2
- x ** 2=9
- --------------
- x=3
复制代码
我们不难发现, 生成器函数计算出 x 的平方后就挂起退出了, 但他仍然保存了此时 x 的值, 而 yield 后的 print 语句会在 for 循环的下一轮迭代中首先调用, 此时 x 的值即是上一轮退出时保存的值.
[妹子说] 那再说说生成器表达式吧.
列表解析式已经是一个不错的选择, 从内存使用的角度而言, 生成器更优, 因为他不用一次性生成整个对象列表, 这二者之间如何转化呢?
生成器表达式写法上很像列表解析式, 但是外面的方括号换成了圆括号, 结果大不同
简单的看看:
- print([x ** 2 for x in range(5)])
- print((x ** 2 for x in range(5)))
- [0, 1, 4, 9, 16]
- <generator object <genexpr> at 0x0000000002212558>
复制代码
方括号是熟悉的列表解析式, 一次性返回整个列表, 圆括号是生成器表达式, 返回一个生成器对象, 而不是一次性生成整个列表.
同时他支持迭代协议, 适用于所有的迭代环境:
略举几个例子:
- for x in (x ** 2 for x in range(5)):
- print(x, end=',')
- 0,1,4,9,16,
复制代码
- -
- print(sum(x ** 2 for x in range(5)))
- 30
复制代码
- -
- print(sorted((x ** 2 for x in range(5)), reverse=True))
- [16, 9, 4, 1, 0]
复制代码
- -
- print(list(x ** 2 for x in range(5)))
- [0, 1, 4, 9, 16]
复制代码
总结: 生成器表达式是对内存空间的优化. 他们不需要像方括号的列表解析一样, 一次构造出整个结果列表. 他们运行起来比列表解析式可能稍慢一些, 因此他们对于非常大的结果集合运算是最优的选择.
[妹子说] 那总结起来一句话: 列表解析式最快, 生成器表达式最省空间, 速度也还可以.
补充说明一下:
集合解析式等效于将生成器对象传入到 list,set,dict 等函数中作为构造参数
- set(f(x) for x in S if P(x))
- {f(x) for x in S if P(x)}
- {key:val for (key, val) in zip(keys, vals)}
- dict(zip(keys, vals))
- {x:f(x) for x in items}
- dict((x, f(x)) for x in items)
复制代码
来源: https://juejin.im/post/5b61a2ece51d4518f5443569