Python生成器的使用技巧详解

作者:酱油哥,清华大学计算机硕士,泰康资管/军工央企职场经历。

0.本集概览

1.生成器可以避免一次性生成整个列表 2.生成器函数的运行过程解析及状态保存 3.生成器表达式的使用方法 4.生成器表达式的可迭代特性

之前我们介绍了列表解析式,他的优点很多,比如运行速度快、编写简单,但是有一点我们不要忘了,他是一次性生成整个列表。如果整个列表非常大,这对内存也同样会造成很大压力,想要实现内存的节约,可以将列表解析式转换为生成器表达式。

今天这一集,就单聊生成器。

1.避免一次性生成整个列表

避免一次性生成整个结果列表的本质是在需要的时候才逐次产生结果,而不是立即产生全部的结果,Python中有两种语言结构可以实现这种思路。

一个是生成器函数。外表看上去像是一个函数,但是没有用return语句一次性的返回整个结果对象列表,取而代之的是使用yield语句一次返回一个结果。

另一个是生成器表达式。类似于上一小节的列表解析,但是方括号换成了圆括号,他们返回按需产生的一个结果对象,而不是构建一个结果列表。

这个“按需”指的是在迭代的环境中,每次迭代按需产生一个对象,因此,上述二者都不会一次性构建整个列表,从而节约了内存空间。

2.生成器函数

下面具体结合例子说说生成器函数。

2.1.运行过程分析

首先,我们还没有详细介绍过函数,先简单说一下,常规函数接受输入的参数然后立即送回单个结果,之后这个函数调用就结束了。

但生成器函数却不同,他通过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

2.2.状态保存

我们进一步来说说生成器函数里状态保存的话题。在每次循环的时候,生成器函数都会在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的值即是上一轮退出时保存的值。

3.生成器表达式

再说说生成器表达式吧。

3.1.使用方法

列表解析式已经是一个不错的选择,从内存使用的角度而言,生成器更优,因为他不用一次性生成整个对象列表,这二者之间如何转化呢?

生成器表达式写法上很像列表解析式,但是外面的方括号换成了圆括号,结果大不同,简单的看看: 代码片段:

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>

方括号是熟悉的列表解析式,一次性返回整个列表,圆括号是生成器表达式,返回一个生成器对象,而不是一次性生成整个列表

3.2.适用于迭代环境

同时他支持迭代协议,适用于所有的迭代环境,略举几个例子: 代码片段:

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]

3.3.集合解析式与生成器对象

集合解析式等效于将生成器对象传入到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编程语言核心基础》小册子中的一篇文章,小册共分12小节。点击下面进入小册子,原创不易,欢迎订阅:

小册目录

第1节:深入剖析 Python 容器的使用方法

第2节:循环迭代与容器遍历用法解析

第3节:详解字符串常见用法

第4节:Python字符编码深入剖析及应用举例

第5节:Python文件操作用法探讨

第6节:Python 动态类型与对象拷贝机制分析

第7节:理顺可迭代对象、迭代器与迭代环境

第8节:生成器的使用技巧详解

第9节:函数的基本特征与变量作用域

第10节:函数参数的传递、修改、匹配与解包过程全解析

第11节:函数闭包与装饰器用法详解

第12节:异常的处理方式

原文发布于微信公众号 - Python中文社区(python-china)

原文发表时间:2018-07-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Web 开发

JavaScript的对象引用

在一个函数体内,var变量声明的变量,其作用域只在该函数体内,对于函数体外而言,是不可见的(废话)。

840
来自专栏北京马哥教育

对Python老司机99%有帮助的简明语法总结乱编

本文由马哥教育Python实战开发班6期学员推荐,转载自互联网,作者为赖笔小新,感谢作者的辛苦付出和贡献。 最近发现进入python群的朋友都在你是如何自学py...

3127
来自专栏青玉伏案

窥探Swift之使用Web浏览器编译Swift代码以及Swift中的泛型

   有的小伙伴会问:博主,没有Mac怎么学Swift语言呢,我想学Swift,但前提得买个Mac。非也,非也。如果你想了解或者初步学习Swift语言的话,你可...

1935
来自专栏猿人谷

求子数组的最大和

分析:输入一个整形数组,数组里有正数也有负数,数组中一个或连续的多个正数,求所有子数组的和的最大值。 当我们加上一个正数时,和会增加;当我们加上一个负数时,和会...

19910
来自专栏desperate633

LeetCode 7. Reverse Integer分析代码

832
来自专栏求索之路

Effective Java笔记(不含反序列化、并发、注解和枚举)

最近把Effective Java复习了一遍,其中有比较多的java最佳实践可以在平时编程中用到。反序列化、并发、注解和枚举这四章没看,并发这本书里讲的比较简...

35911
来自专栏北京马哥教育

Python2.x与3​​.x版本区别

? 文 | 豌豆 来源 | 菜鸟教程 Python的3.0版本,常被称为Python 3000,或简称Py3k。相对于Python的早期版本,这是一个较...

3496
来自专栏C/C++基础

C++ struct与union

编码运行环境:VS2012+Win32+Debug Win32既表示运行平台是Windows 32bits操作系统,又表示生成32bits的应用程序。

831
来自专栏决胜机器学习

Redis专题(二) ——Redis数据类型(2)

Redis专题(二)——Redis数据类型(2) (原创内容,转载请注明来源,谢谢) 四、列表类型(List) 列表类型可以存储一个有序的字符串列表,其存储...

3476
来自专栏前端进阶之路

JavaScript数据结构03 - 队列

队列是遵循FIFO(First In First Out,先进先出)原则的一组有序的项。队列在尾部添加新元素,并从顶部移除元素。最新添加的元素必须排在队列的末尾...

1021

扫码关注云+社区

领取腾讯云代金券