返回介绍

7.5 自上而下

发布于 2023-06-02 10:04:34 字数 19909 浏览 0 评论 0 收藏 0

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文