查看原文
其他

给妹子讲python-S01E16生成器的使用

给妹子讲python Python爱好者社区 2019-04-07

作者:酱油哥/ 清华程序猿      
微信公众号: python数据科学家
知乎专栏: 《给妹子讲python》
https://zhuanlan.zhihu.com/c_147297848



前文传送门:

给妹子讲python-S01E01好用的列表

给妹子讲python-S01E02学会用字典

给妹子讲python-S01E03元组的使用

给妹子讲python-S01E04容器遍历和列表解析式

给妹子讲python-S01E05字符串的基本用法

给妹子讲python-S01E06字符串用法进阶

给妹子讲python-S01E07字符编码历史观:从ASCII到Unicode

给妹子讲python-S01E08理清python中的字符编码方法

给妹子讲python-S01E09文件操作小意思

给妹子讲python-S01E10动态类型与共享引用

给妹子讲python-S01E11赋值与对象拷贝

给妹子讲python-S01E12循环迭代初体验

给妹子讲python-S01E13循环迭代高级技巧

给妹子讲python-S01E14可迭代对象和迭代器

给妹子讲python-S01E15迭代环境

【要点抢先看】

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)



Python爱好者社区历史文章大合集

Python爱好者社区历史文章列表(每周append更新一次)

福利:文末扫码立刻关注公众号,“Python爱好者社区”,开始学习Python课程:

关注后在公众号内回复“课程”即可获取:

小编的Python入门免费视频课程!!!

【最新免费微课】小编的Python快速上手matplotlib可视化库!!!

崔老师爬虫实战案例免费学习视频。

陈老师数据分析报告制作免费学习视频。

玩转大数据分析!Spark2.X+Python 精华实战课程免费学习视频。


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存