机械工程师Python编程:入门、实战与进阶
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.1.6 filter、map和reduce函数

在函数式编程中,我们从不修改集合中的元素,而是创建一个新的集合来反映对该集合的操作的更改。有三个操作构成了函数式编程的基石,而且可以实现对集合的、我们能想到的任何修改:filter、map和reduce。

1. filter函数

filter函数接收一个集合,过滤掉某些元素并生成一个新集合。元素的过滤是根据判定函数进行,判定函数会接受参数,根据该参数是否通过给定的测试来返回True或False。

图2-1说明了过滤器的操作。

图2-1 过滤一个元素集

图2-1显示了由A、B、C和D四个元素组成的元素集。元素集下面是一个代表判定函数的框,它决定哪些元素被保留,哪些元素被丢弃。元素集中的每个元素都被传递给该函数,只有通过测试的元素才会出现在结果集合中。

Python有两种方法可以过滤集合:一,使用全局函数filter;二,如果集合是一个列表,使用列表推导式。这里我们主要关注filter函数,下一节会介绍列表推导式。filter函数的输入参数是一个函数(判定)和一个集合:

让我们写一个lambda判定函数来测试一个数字是否为偶数:

现在让我们使用lambda函数来过滤一个数字列表,并获得一个只有偶数的新集合:

需要注意的是,filter函数并不会返回列表,而是返回迭代器。迭代器允许对一组元素进行依次迭代。如果你想了解更多关于Python迭代器及其底层原理,请参阅https://docs.python.org/3/library/stdtypes.html#typeiter和https://docs.python.org/3/glossary.html#termiterator上的文档。

我们可以使用前面看到的list函数使用所有迭代器的值,并将它们放入一个列表中,也可以使用for循环来使用迭代器:

2. map函数

map函数对原集合中的每个元素进行函数运算,并将结果存储到一个新的元素集中。两个元素集的大小相同。

图2-2描绘了映射操作。

图2-2 映射一个元素集

我们通过一个映射函数,对元素A、B、C和D组成的源集合进行运算,如图2-2中的五边形所示,运算的结果存储在一个新的元素集中。

我们可以使用全局函数map来映射一个元素集,对于列表,还可以使用列表推导式。我们稍后将讨论列表推导式,现在,让我们研究如何使用map函数来映射元素集。

全局函数map接收两个参数,即一个映射函数和一个源集合:

以下代码将一个名称列表与它们的长度进行映射:

与filter函数一样,map返回一个迭代器,可以使用list函数生成列表。在上面的示例中,结果列表包含了名称列表中每个名称的字符数:Angel对应5个字符,Alvaro对应6个字符,以此类推。这样就把每个名称映射成了表示其长度的数字。

3. reduce函数

reduce函数是三个函数中最复杂,但同时用途最广泛的。它可以创建一个少于、多于或等于原集合的元素数量的元素集。为构造这个新的元素集,它首先对第一和第二个元素应用reduce函数;然后,对第三个元素和第一次操作的结果再次应用reduce函数;接着,对第四个元素和第二次操作的结果再次应用reduce函数。这样一来,结果就会累积起来。一个图在这里会有所帮助。请看图2-3。

本例中的reduce函数将元素集中的每个元素(A、B、C和D)累积为单个元素:ABCD。

图2-3 将reduce函数用于一个元素集

reduce函数接收两个参数,即累积结果和元素集中的一个元素:

该函数在处理完新元素后返回累积结果。

Python没有提供全局函数reduce,但是有一个叫functools的包,里面有一些处理高阶函数的有用操作,包括reduce函数。这个函数不返回迭代器,而是直接返回生成的元素集或元素。函数的语法如下:

让我们看一个例子:

在这个示例中,reduce函数返回元素“ABCD”,即元素集中的所有字母连接起来的结果。reduce过程开始时,先接收前两个字母A和B,并将它们连接成AB。对于第一步而言,Python使用集合的第一个元素(A)作为累积结果,并将它和第二个元素上应用reduce函数。然后,它移动到第三个字母C,并将其与当前的累积结果AB连接起来,从而生成新的结果:ABC。最后一步,对D字母同样操作,生成最终结果ABCD。

当累积结果和元素集的元素类型不同时会发生什么?在这种情况下,我们不能将第一个元素作为累积结果,因此reduce函数需要我们提供第三个参数作为初始累积结果:

例如,假设我们想将前面用到的名称集合进行缩减,以获得这些名称长度的总和。在这种情况下,累积结果是数字,而集合中的元素是字符串,我们不能使用第一项作为累积的长度。如果我们忘记给reduce函数提供初始结果,Python会弹出一个错误来提醒我们:

这种情况,我们应该传递0作为初始累积长度:

一个有趣的点在于,如果累积结果和集合元素的类型不同,我们总可以用map函数连接reduce函数以获得相同的结果。例如,在前面的练习中,我们也可以这么做:

在这段代码中,我们首先将列表names映射到一个名称长度的列表lengths中。然后,我们缩减列表lengths来求和所有的值,而不需要提供起始值。

当使用一个常规操作来缩减元素——如两个数字求和或两个字符串的连接——我们不需要编写lambda函数;可以直接将现有的Python函数传递给reduce函数。例如,在缩减数字时,Python提供了一个有用的模块,名为operator.py。这个模块定义了对数字进行操作的函数。使用这个模块,我们可以简化之前的代码如下:

这个代码更短,更易读,因此我们在本书中会倾向于使用这种形式。

operator.add函数由Python定义如下:

如你所见,这个函数等价于我们之前定义的两个数字求和的lambda函数。后面我们会看到更多由Python定义的,可以和reduce函数一起使用的函数。

到目前为止,我们所有的示例都将元素集合缩减到一个值,但reduce函数可以做得更多。事实上,filter和map函数都是reduce函数的特例。我们可以使用reduce函数来过滤和映射一个元素集。但我们并不会在这里停下来分析它;如果你有兴趣,可以试着自己弄清楚。

让我们看一个例子,我们希望基于列表names创建一个新的集合,其中的每个元素都是前面所有的名称与当前名称的组合,由连接符(-)进行分隔。结果类似如下:

我们可以使用以下代码来做到这一点:

这里,我们使用compute_next_name来确定列表中的下一个项。reduce函数内部的lambda函数将累积结果连接起来,生成由组合名称形成的列表,和一个由新元素组成的新列表。需要提供空列表作为初始结果,因为列表中的每个元素的类型(字符串)与结果的类型(字符串组成的列表)不同。

如你所见,reduce函数用途非常广泛。