一:列表解析
列表解析(List comprehensions,或缩略为 list comps )来自函数式编程语言Haskell 。它可以用来动态地创建列表。它在 Python 2.0 中被加入。
列表解析的语法: [expr foriter_var in iterable]
这个语句的核心是 for 循环,它迭代 iterable 对象的所有条目。前边的 expr 应用于序列的每个成员,最后的结果值是该表达式产生的列表。
比如一个计算序列成员的平方的 lambda 函数表达式:
>>> map(lambda x: x ** 2, range(6))
[0,1,4,9,16,25]
可以使用下面这样的列表解析来替换它:
>>> [x ** 2 for x in range(6)]
[0,1,4,9,16,25]
在新语句中,只有一次函数调用( range() ),而先前的语句中有三次函数调用(range() ,map() ,以及 lambda )。列表解析的表达式的效率更高。
结合if语句,列表解析还提供了一个扩展版本的语法:[expr foriter_var in iterable if cond_expr]
这个语法在迭代时会过滤/捕获满足条件表达式 cond_expr 的序列成员。
比如用于判断一个数值对象是奇数还是偶数的函数(奇数返回 1 ,偶数返回 0 ):
def odd(n):
return n % 2
可以使用filter() 和 lambda 挑选出序列中的奇数:
>>> seq = [11, 10, 9, 9, 10, 10, 9,8, 23, 9, 7, 18, 12, 11, 12]
>>> filter(lambda x: x % 2, seq)
[11,9,9,9,23,9,7,11]
也可以使用列表解析来完成操作,获得想要的数字:
>>> [x for x in seq if x % 2]
[11,9,9,9,23,9,7,11]
更多的例子:
迭代一个有三行五列的矩阵么:
>>> [(x+1, y+1) for x in range(3) for y inrange(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)]
计算纯英文文本文件中,所有非空白字符的数目:
可以像这样计算单词个数:
>>> f = open('hhga.txt’, 'r')
>>> len([word for line in f forword in line.split()])
91
可以把每个单词的长度加起来,得到和:
>>> f.seek(0)
>>> sum([len(word) for line in f for word in line.split()])
408
一个清晰明了的列表解析完成了之前需要许多行代码才能完成的工作! 如你所见,列表解析支持多重嵌套for 循环以及多个 if 子句。完整的语法可以在官方文档中找到。也可以在 PEP 202 中找到更多关于列表解析的资料。
二:生成器表达式
生成器表达式是列表解析的一个扩展。
生成器是在 Python 版本 2.2 时加入的一个重要特性。生成器是特定的函数,允许你返回一个值,然后"暂停"代码的执行,稍后恢复。
列表解析的一个不足就是必须生成所有的数据,用以创建整个列表。这在处理有大量数据的迭代器时有负面效应。生成器表达式通过结合列表解析和生成器解决了这个问题。
生成器表达式在 Python 2.4 被引入,它与列表解析非常相似,而且它们的基本语法基本相同。不过它并不真正创建数字列表,而是返回一个生成器。这个生成器在每次计算出一个条目后,把这个条目“产生”(yield)出来。生成器表达式使用了"延迟计算"(lazy evaluation),所以它在使用内存上更有效。
来看看它和列表解析到底有多相似:
列表解析:[exprfor iter_var in iterable if cond_expr]
生成器表达式:(exprfor iter_var in iterable if cond_expr)
在前边列表解析一节,计算文本文件中非空白字符总和。最后的代码中,我们展示了如何使用一行列表解析代码做所有的事。
但是如果这个文件的大小变得很大,那么这行代码的内存性能会很低,因为我们要创建一个很长的列表用于存放单词的长度。
为了避免创建庞大的列表,可以使用生成器表达式来完成求和操作。它会计算每个单词的长度然后传递给 sum() 函数(它的参数不仅可以是列表,还可以是可迭代对象,比如生成器表达式)。这样,可以得到优化后的代码(代码长度,还有执行效率都很高效):
>>> sum(len(word) for line in datafor 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 forj 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)
在举一个冗长的样例,从它可以感觉到 Python 代码在这些年来的变化,看看如何改进代码。一个寻找文件最长的行的例子来,在以前,我们这样读取文件:
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
列表解析允许我们稍微简化我们代码,而且我们可以在得到行的集合前做一定的处理。
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
前两个例子在处理大文件时候都有问题,因为 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 程序也不是很晦涩。可以在 PEP 289 中找到更多生成器表达式相关内容。