本节内容
1、概述
2、列表生成式
3、生成器
4、函数实现生成器
5、生成器表达式
一、概述
我们在使用一组数据时,通常情况下会定义一个列表,然后循环里面的元素,但是你想过没有,如果你只需要使用列表中的1-2个元素,其他的元素用不到,这样就会造成资源的浪费,这样不能很好的合理的利用我们机器的资源,那我们如何合理高效的利用这些利用这些资源,并且提高我们程序的运行速度呢?下面我们就来讲讲我们今天最关键的知识点,生成器。
二、列表生成式
1、定义
看列表[0,1,2,3,4,5,6,7,8,9],需求是把列表中的每个元素加1,你是怎么实现的呐?
1
2
3
4
5
6
7
|
a = [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] for index,i in enumerate (a): a[index] + = 1 print (a) #输出 [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ] |
当然可能还有其他方法,这边就逐一介绍了,我这边有一个最简单的方法:
1
2
|
>>> [ i * 2 for i in range ( 10 )] [ 0 , 2 , 4 , 6 , 8 , 10 , 12 , 14 , 16 , 18 ] |
以上这种就叫列表生成
应用案例
===矩阵样例===
你需要迭代一个有三行五列的矩阵么? 很简单:
>>> [(x+1,y+1) for x in range(3) for y in range(5)] [(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5)]
===磁盘文件样例===
假设我们有如下这样一个数据文件 hhga.txt , 需要计算出所有非空白字符的数目:
And the Lord spake, saying, "First shalt thou take out the Holy Pin. Then shalt thou count to three, no more, no less. Three shall be the number thou shalt count, and the number of the counting shall be three. Four shalt thou not count, nei- ther count thou two, excepting that thou then proceed to three. Five is right out. Once the number three, being the third number, be reached, then lobbest thou thy Holy Hand Grenade of Antioch towards thy foe, who, being naughty in My sight, shall snuff it."
我们已经知道可以通过 for line in data 迭代文件内容, 不过, 除了这个, 我们还可以把每
行分割( split )为单词, 然后我们可以像这样计算单词个数:
>>> f = open('hhga.txt', 'r') >>> len([word for line in f for word in line.split()]) 91
快速地计算文件大小
import os >>> os.stat('hhga.txt').st_size 499L
假定文件中至少有一个空白字符, 我们知道文件中有少于 499 个非空字符. 我们可以把每个
单词的长度加起来, 得到和.
>>> f.seek(0) >>> sum([len(word) for line in f for word in line.split()]) 408
这里我们用 seek() 函数回到文件的开头, 因为迭代器已经访问完了文件的所有行. 一个清晰
明了的列表解析完成了之前需要许多行代码才能完成的工作! 如你所见, 列表解析支持多重嵌套
for 循环以及多个 if 子句. 完整的语法可以在官方文档中找到. 你也可以在 PEP 202 中找到更多
关于列表解析的资料.
三、生成器
正如我之前所说的,我们可以通过列表生成式,直接去创建一个列表。但是收到内存的限制,列表的容量是有限的。如果我们在创建一个包含100万个元素的列表,甚至更多,不仅占用了大量的内存空间,而且如果我们仅仅需要访问前面几个元素时,那后面很大一部分的占用的空间都白白浪费掉了。这个并不是我们所希望看到的。
所以我们就诞生了一个新的名词叫生成器:generator。下面我们就来说说这个生成器的作用。
生成器的作用:列表的元素按某种算法推算出来,我们在后续的循环中不断推算出后续的元素,在python中,这种一边循环一边计算的机制,称之为生成器(generator)。
1、创建生成器
1
2
3
4
5
6
|
>>> m = [i * 2 for i in range ( 10 )] >>> m [ 0 , 2 , 4 , 6 , 8 , 10 , 12 , 14 , 16 , 18 ] #生成一个list >>> n = (i * 2 for i in range ( 10 )) >>> n <generator object <genexpr> at 0x00000000033A4FC0 > #生成一个generator |
如果需要访问生成器n中的值,python2是通过next()方法去获得generator的下一个返回值,python3是通过__next__()去获得generator的下一个返回值:
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
27
28
29
30
31
32
33
34
35
|
#python 3的访问方式用__next__() >>> n.__next__() 0 >>> n.__next__() 2 >>> n.__next__() 4 >>> n.__next__() 6 >>> n.__next__() 8 >>> n.__next__() #没有元素时,则会抛出抛出StopIteration的错误 Traceback (most recent call last): File "<pyshell#4>" , line 1 , in <module> n.__next__() StopIteration #python2的访问方式用next() >>> n. next () #可以用n.next() 0 >>> next (n) #也可以用next(n) 2 >>> n. next () 4 >>> n. next () 6 >>> n. next () 8 >>> n. next () #没有元素时,则会抛出抛出StopIteration的错误 Traceback (most recent call last): File "<pyshell#4>" , line 1 , in <module> n. next () StopIteration |
小结:①generator保存的是算法,每次调用next方法时,就会计算下一个元素的值,直到计算到最后一个元素,如果没有更多元素,则会抛出StopIteration的错误。
②generator只记住当前位置,它访问不到当前位置元素之前和之后的元素,之前的数据都没有了,只能往后访问元素,不能访问元素之前的元素。
2、用for循环去访问generator中的元素
用next方法去一个一个访问,仿佛有点变态,也不切实际,正确的方法是使用for循环去访问,因为generator也是可迭代对象,代码如下:
1
2
3
4
5
6
7
8
9
10
|
>>> res = (i * 2 for i in range ( 3 )) #创建一个生成器 >>> res <generator object <genexpr> at 0x0000000003155C50 > >>> for i in res: #迭代生成器中的元素 print (i) #输出 0 2 4 |
所以我们在创建一个生成器以后,基本不会用next方法去访问,而是通过for循环来迭代它,并且更不用关心StopIteration错误。
四、函数实现生成器
上面推算比较简单,但是推算的算法比较复杂,用类似列表生成式的for循环无法实现,那怎么办呢?比如下面一个例子,用列表生成式无法实现。
1、斐波那契数列
实现原理:除第一个和第二个数外,任意一个数都可由前两个数相加得到:1, 1, 2, 3, 5, 8, 13, 21, 34, ...,代码如下:
1
2
3
4
5
6
7
8
|
def fib( max ): n,a,b = 0 , 0 , 1 while n < max : print (b) a , b = b ,a + b n = n + 1 return "----done---" |
很明显斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易。
这边需要注意的是赋值语句:
1
|
a , b = b ,a + b |
相当于:
1
2
3
|
t = (b, a + b) # t是一个tuple a = t[ 0 ] b = t[ 1 ] |
上面的执行结果:
1
2
3
4
5
6
7
8
|
fib( 5 ) #执行结果 1 1 2 3 5 - - - - done - - - |
根据这种逻辑推算非常类似一个生成器(generator)。但是怎么把一个函数转换成一个生成器呢?
2、用yield函数转换为生成器(generator)
1
2
3
4
5
6
7
8
|
def fib( max ): n,a,b = 0 , 0 , 1 while n < max : yield b #用yield替换print,把fib函数转化成一个生成器 a , b = b ,a + b n = n + 1 return "----done---" |
以上就是生成器(generator)另外一种定义方法。如果一个函数中包含yield关键字,那么这个函数就不是一个普通的函数,而是一个生成器(generator)。
1
2
3
4
5
|
f = fib( 5 ) print (f) #输出 <generator object fib at 0x0000000000D1B4C0 > |
这边有两个难理解地方:①函数是顺序执行的,遇到return
语句或者最后一行函数语句就返回
②变成generator的函数,在每次调用next()
的时候执行,遇到yield
语句返回,再次执行时从上次返回的yield
语句处继续执行。
①访问元素:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
f = fib( 5 ) print (f.__next__()) print (f.__next__()) print (f.__next__()) print ( "我在干别的事情" ) print (f.__next__()) print (f.__next__()) #访问的是最后一个元素 print (f.__next__()) #没有多余的元素 #输出 1 1 2 - - - - - 我在干别的事情 - - - - - 3 5 Traceback (most recent call last): File "D:/PycharmProjects/pyhomework/day4/生成器/fib.py" , line 20 , in <module> print (f.__next__()) StopIteration: - - - - done - - - |
从上面的例子可以看出来:①我访问生成器中的元素,不用是连续的,我可以中间去执行其他程序,向想什么时候执行,可以再回头去执行。
②return在这边作用就是当发生异常时,会打印ruturn后面的值。
②for循环访问
1
2
3
4
5
6
7
8
9
10
|
f = fib( 5 ) for i in f: print (i) #输出 1 1 2 3 5 |
③捕获这个StopIteration这个异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
f = fib( 5 ) while True : try : x = f.__next__() print ( "f:" ,x) except StopIteration as e: #当try中的程序执行错误了,才会执行except下面的代码 print ( "Generator return value:" ,e.value) break #执行结果 f: 1 f: 1 f: 2 f: 3 f: 5 Generator return value: - - - - done - - - |
关于这个异常处理,后续会继续发博客更新,请大家敬请期待。。。。。
五、生成器表达式
===磁盘文件样例===
在前边列表解析一节, 我们计算文本文件中非空白字符总和. 最后的代码中, 我们展示了如何
使用一行列表解析代码做所有的事. 如果这个文件的大小变得很大, 那么这行代码的内存性能会很
低, 因为我们要创建一个很长的列表用于存放单词的长度.
为了避免创建庞大的列表, 我们可以使用生成器表达式来完成求和操作. 它会计算每个单词的
长度然后传递给 sum() 函数(它的参数不仅可以是列表,还可以是可迭代对象,比如生成器表达式).
这样, 我们可以得到优化后的代码(代码长度, 还有执行效率都很高效):
>>> sum(len(word) for line in data for word in line.split()) 408
我们所做的只是把方括号删除: 少了两字节, 而且更节省内存 ... 非常地环保!
=== 交叉配对例子 ===
生成器表达式就好像是懒惰的列表解析(这反而成了它主要的优势). 它还可以用来处理其他列
表或生成器, 例如这里的 rows 和 cols :
rows = [1, 2, 3, 17] def cols(): # example of simple generator yield 56 yield 2 yield 1
不需要创建新的列表, 直接就可以创建配对. 我们可以使用下面的生成器表达式:
x_product_pairs = ((i, j) for i in rows for j in cols())
现在我们可以循环 x_product_pairs , 它会懒惰地循环 rows 和 cols :
>>> for pair in x_product_pairs: ... print pair ... (1, 56) (1, 2) (1, 1) (2, 56) (2, 2) (2, 1) (3, 56) (3, 2) (3, 1) (17, 56) (17, 2) (17, 1)
=== 重构样例 ===
我们通过一个寻找文件最长的行的例子来看看如何改进代码. 在以前, 我们这样读取文件:
f = open('/etc/motd', 'r') longest = 0 while True: linelen = len(f.readline().strip()) if not linelen: break if linelen > longest: longest = linelen f.close() return longest
事实上, 这还不够老. 真正的旧版本 Python 代码中, 布尔常量应该写是整数 1 , 而且我们应
该使用 string 模块而不是字符串的 strip() 方法:
import string : len(string.strip(f.readline()))
从那时起, 我们认识到如果读取了所有的行, 那么应该尽早释放文件资源. 如果这是一个很多
进程都要用到的日志文件, 那么理所当然我们不能一直拿着它的句柄不释放. 是的, 我们的例子是
用来展示的, 但是你应该得到这个理念. 所以读取文件的行的首选方法应该是这样:
f = open('/etc/motd', 'r') longest = 0 allLines = f.readlines() f.close() for line in allLines: linelen = len(line.strip()) if linelen > longest: longest = linelen return longest
列表解析允许我们稍微简化我们代码, 而且我们可以在得到行的集合前做一定的处理. 在下段
代码中, 除了读取文件中的行之外,我们还调用了字符串的 strip() 方法处理行内容.
f = open('/etc/motd', 'r') longest = 0 allLines = [x.strip() for x in f.readlines()] f.close() for line in allLines: linelen = len(line) if linelen > longest: longest = linelen return longest Edit By Vheavens Edit By Vheavens
然而, 两个例子在处理大文件时候都有问题, 因为 readlines() 会读取文件的所有行. 后来
我们有了迭代器, 文件本身就成为了它自己的迭代器, 不需要调用 readlines() 函数. 我们已经
做到了这一步, 为什么不去直接获得行长度的集合呢(之前我们得到的是行的集合)? 这样, 我们就
可以使用 max() 内建函数得到最长的字符串长度:
f = open('/etc/motd', 'r') allLineLens = [len(x.strip()) for x in f] f.close() return max(allLineLens)
这里唯一的问题就是你一行一行迭代 f 的时候, 列表解析需要文件的所有行读取到内存中,
然后生成列表. 我们可以进一步简化代码: 使用生成器表达式替换列表解析, 然后把它移到 max()
函数里, 这样, 所有的核心部分只有一行:
f = open('/etc/motd', 'r') longest = max(len(x.strip()) for x in f) f.close() return longest
最后, 我们可以去掉文件打开模式(默认为读取), 然后让 Python 去处理打开的文件. 当然,
文件用于写入的时候不能这么做, 但这里我们不需要考虑太多:
return max(len(x.strip()) for x in open('/etc/motd'))
我们走了好长一段路. 注意,即便是这只有一行的 Python 程序也不是很晦涩. 生成器表达式
在 Python 2.4 中被加入, 你可以在 PEP 289 中找到更多相关内容.