7.5 自上而下
1.便捷表达式
在本章的一开始,我们就提到函数式编程的思维是自上而下式的。Python中也有不少体现这一思维的语法,如生成器表达式、列表解析和词典解析。生成器表达式是构建生成器的便捷方法。考虑下面一个生成器:
def gen():
for i in range(4):
yield i
等价的,上面程序可以写成生成器表达式(Generator Expression):
gen = (x for x in range(4))
这一语法很直观,写出来的代码也很简洁。
我们再来看看生成一个列表的方法:
l = []
for x in range(10):
l.append(x**2)
上述代码生成了表l,但有更快的方式。列表解析(List Comprehension)是快速生成列表的方法。它的语法简单,很有实用价值。列表解析的语法和生成器表达式很像,只不过把小括号换成了中括号:
l = [x**2 for x in range(10)]
列表解析的语法很直观。我们直截了当地说明了想要的是元素的平方,然后再通过for来增加限定条件,即哪些元素的平方。除了for,列表解析中还可以使用if。比如下面一个更复杂的例子:
xl = [1,3,5]
yl = [9,12,13]
l = [ x**2 for (x,y) in zip(xl,yl) if y > 10]
词典解析可用于快捷的生成词典。它的语法也与之前的类似:
d = {k: v for k,v in enumerate("Vamei") if val not in "Vi"}
你大概猜出它的结果了,可以在Python上验证一下。
2.懒惰求值
Python中的迭代器也很有函数式编程的意味。从功能上说,迭代器很多时候看起来就像一个列表。比如下面的迭代器和列表,效果上都一样:
for i in (x**2 for x in range(10)):
print(i)
for i in [x**2 for x in range(10)]:
print(i)
但我们在介绍迭代器时曾提到过,迭代器的元素是实时计算出来的。在使用该元素之前,元素并不会占据内存空间。与之相对应,列表在建立时,就已经产生了各个元素的值,并保存在内存中。迭代器的工作方式正是函数式编程中的懒惰求值(Lazy Evaluation)。我们可以对迭代器进行各种各样的操作。但只有在需要时,迭代器才会计算出具体的值。
懒惰求值可以最小化计算机要做的工作。比如下面的程序可以在Python 3中飞速运行完成:
a = range(100000000)
result = map(lambda x: x**2, a)
在Python 3中,上面程序可以迅速执行。因为map返回的是迭代器,所以会懒惰求值^(4)^{#ch4-back}。更早之前的range调用返回的同样是迭代器,也是懒惰求值。除非通过某种方式调用迭代器中的元素,或者把迭代器转化成列表,运算过程才会开始。因此,在下面的程序中,如果把结果转化成列表,那么运算时间将大为增加。
a = range(100000000)
result = map(lambda x: x**2, a)
result = list(result)
如果说计算最终都不可避免,那么懒惰求值和即时求值的运算量并没有什么差别。但如果不需要穷尽所有的数据元素,那么懒惰求值将节省不少时间。比如下面的情况中,列表提前准备数据的方式,就浪费了很多运算资源:
for i in (x**2 for x in range(100000000)):
if i>1000:
break;
for i in [x**2 for x in range(100000000)]:
if i>1000:
break;
除了运算资源,懒惰求职还能节约内存空间。对于即时求职来说,其运算过程的中间结果都需要占用不少的内存空间。而懒惰求值可以先在迭代器层面上进行操作,在获得最终迭代器以后一次性完成计算。除了用map()、filter()等函数外,Python中的itertools包还提供了丰富的操作迭代器的工具。
3.itertools包
标准库中的itertools包提供了更加灵活的生成迭代器的工具,这些工具的输入大都是已有的迭代器。另一方面,这些工具完全可以自行使用Python实现,该包只是提供了一种比较标准、高效的实现方式。这也符合Python“只有且最好只有一个解决方案”的理念。
#引入itertools
from itertools import *
这个包中提供了很多有用的生成器。下面两个生成器能返回无限循环的迭代器:
count(5, 2) #从5开始的整数迭代器,每次增加2,即5, 7, 9, 11, 13 ...
cycle("abc") #重复序列的元素,既a, b, c, a, b, c ...
repeat()既可以返回一个不断重复的迭代器,也可以是有次数限制的重复:
repeat(1.2) #重复1.2,构成无穷迭代器,即1.2, 1.2, 1.2, ...
repeat(10, 5) #重复10,共重复5次
我们还能组合旧的迭代器,来生成新的迭代器:
chain([1, 2, 3], [4, 5, 7]) # 连接两个迭代器成为一个。1, 2, 3, 4, 5, 7
product("abc", [1, 2]) # 多个迭代器集合的笛卡儿积。相当于嵌套循环
所谓的笛卡儿积可以得出集合元素所有可能的组合方式:
for m, n in product("abc", [1, 2]):
print(m, n)
如下所示:
permutations("abc", 2) # 从"abcd"中挑选两个元素,比如ab, bc, ……
# 将所有结果排序,返回为新的迭代器。
#上面的组合区分顺序,即ab, ba都返回。
combinations("abc", 2) # 从"abcd"中挑选两个元素,比如ab, bc, ……
# 将所有结果排序,返回为新的迭代器。
# 上面的组合不区分顺序,
# 即ab, ba,只返回一个ab。
combinations_with_replacement("abc", 2) # 与上面类似,
# 但允许两次选出的元素重复。即多了aa, bb, cc
itertools包中还提供了许多有用的高阶函数:
starmap(pow, [(1, 1), (2, 2), (3, 3)]) # pow将依次作用于表的每个tuple。
takewhile(lambda x: x < 5, [1, 3, 6, 7, 1]) # 当函数返回True时,
#收集元素到迭代器。一旦函数返回False,则停止。1, 3
dropwhile(lambda x: x < 5, [1, 3, 6, 7, 1]) # 当函数返回False时,
#跳过元素。一旦函数返回True,则开始收集剩下的所有元素到迭代器。6, 7, 1
包中提供了groupby()函数,能将一个key()函数作用于原迭代器的各个元素,从而获得各个函数的键值。根据key()函数结果,将拥有元素分组。每个分组中的元素都保罗了键值形同的返回结果。函数groupby()分出的组放在一个迭代器中返回。
如果有一个迭代器,包含一群人的身高。我们可以使用这样一个key()函数: 如果身高大于180,返回"tall";如果身高低于160,返回"short";中间的返回"middle"。最终,所有身高将分为三个迭代器,即"tall"、"short"、"middle"。
from itertools import groupby
def height_class(h):
if h > 180:
return "tall"
elif h < 160:
return "short"
else:
return "middle"
friends = [191, 158, 159, 165, 170, 177, 181, 182, 190]
friends = sorted(friends, key = height_class)
for m, n in groupby(friends, key = height_class):
print(m)
print(list(n))
注意,groupby()的功能类似于UNIX中的uniq命令。分组之前需要使用sorted()对原迭代器的元素,根据key()函数进行排序,让同组元素先在位置上靠拢。
这个包中还有其他一些工具,方便迭代器的构建:
compress("ABCD", [1, 1, 1, 0]) #根据[1, 1, 1, 0]的真假值情况,选择
#保留第一个参数中的元素。A, B, C
islice() # 类似于slice()函数,只是返回的是一个迭代器
izip() # 类似于zip()函数,只是返回的是一个迭代器。
至此,本书介绍了Python中包含的函数式编程特征:作为第一级对象的函数、闭包、高阶函数、懒惰求值……这些源于函数式编程的语法,以函数核心提供了一套新的封装方式。当我们编程时,可以在面向过程和面向对象之外,提供一个新的选择,从而给程序更大的创造空间。函数式编程自上而下的思维,也给我们的编程带来更多启发。在并行运算的发展趋势下,函数式编程正走向繁荣。希望本章的内容能对你的函数式编程学习有所助益。
————————————————————
(1){#ch1} 在Python 2.7中,map()返回的是一个列表
(2){#ch2} Python 2.7中,reduce()是内置函数,不需要引入。
(3){#ch3} 参看http://research.google.com/archive/mapreduce.html
(4){#ch4} Python 2.7中,range()和map()返回的都是列表,所以是即时求值。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论